Winter is a lightweight dependency injection library for Android and the JVM written in Kotlin.

Installation

dependencies {
    // Core
    implementation 'io.jentz.winter:winter:x.x.x'
    // AndroidX support
    implementation 'io.jentz.winter:winter-androidx:x.x.x'
    // RxJava2 support module
    implementation 'io.jentz.winter:winter-rxjava2:x.x.x'
    // Java support module
    implementation 'io.jentz.winter:winter-java:x.x.x'
    // JUnit4 test support
    implementation 'io.jentz.winter:winter-junit4:x.x.x'
    // JUnit5 test support
    implementation 'io.jentz.winter:winter-junit5:x.x.x'
    // JSR-330 support
    kapt 'io.jentz.winter:winter-compiler:x.x.x'
}

Replace x.x.x with the version you would like to use. The latest version is 0.8.0.

Available Modules

Module Description API Doc

io.jentz.winter:winter:0.8.0

Winter core module

Winter

io.jentz.winter:winter-junit4:0.8.0

JUnit4 support

Winter JUnit4

io.jentz.winter:winter-junit5:0.8.0

JUnit5 support

Winter JUnit5

io.jentz.winter:winter-testing:0.8.0

Testing support shared between the JUnit4 and JUnit5 modules

Winter Testing

io.jentz.winter:winter-rxjava2:0.8.0

RxJava2 support

Winter RxJava2

io.jentz.winter:winter-androidx:0.8.0

Android X support

Winter AndroidX

io.jentz.winter:winter-java:0.8.0

Java support

Winter Java

Terminology

Term Definition

Component

The immutable dependency registry for providers and subcomponents.

Subcomponent

A component that is defined inside a component to partition the object graphs into subgraphs.

Graph

The object graph that holds actual instances and is used to retrieve dependencies defined in a component.

Subgraph

Graph created from a subcomponent that can access dependencies from its parent graph but the parent has no access to child dependencies.

Scope

The lifetime of an instance in a graph e.g. singleton for only one per graph or prototype for one each time an instance is requested.

Core

Declaring Dependencies

Dependencies are organized in components and declared by using the component method which takes a block with a Component Builder as its receiver.

You register a dependency with a scope-function like prototype or singleton.

val coffeeAppComponent = component {
    prototype { Heater() }
    prototype<Pump> { RotaryPump() }
    singleton { CoffeeMaker(instance(), instance()) }
}

All scope functions take an optional qualifier for cases where you want to register the same type multiple times and all scope functions take an boolean to enable generic type preservation.

For a list of all builder methods see API docs of Component Builder.

Available Scopes

Scope methods Description

prototype

The factory gets called every time the type is requested.

singleton

The factory is only called the first time the type is requested and then memorized. Every subsequent request will return the same instance.

eagerSingleton

Same as singleton but the factory is called when the dependency graph gets instantiated.

softSingleton

Like singleton but the instance is hold as a SoftReference and could be GC’ed. In case the reference is cleared the factory will be again invoked when the type is requested.

weakSingleton

Like singleton but the instance is hold as a WeakReference and could be GC’ed. In case the reference is cleared the factory will be again invoked when the type is requested.

Subcomponents and Subgraphs

Subcomponents are used to partition the object graph into subgraphs to encapsulate different parts of the application from each other e.g. the business layer from the view layer of an application. Subgraphs inherit and extend the parent graph which means that an service bound in a subgraph can access all services of the parent graph but not vice versa. Subgraphs can have a shorter lifetime than their parents and there can be multiple subgraphs with the same parent and from the same subcomponent.

val coffeeAppComponent = component {
    singleton { HttpCache() }

    subcomponent("gui") {
        singleton { ImageLoader(cache = instance<HttpCache>()) }
    }
}

// initialize the application component
val appGraph = coffeeAppComponent.createGraph()
// open a subgraph
val guiGraph = appGraph.openSubgraph("gui")
// close a subgraph
appGraph.closeSubgraph("gui")
// or
guiGraph.close()

In this example guiGraph can access HttpCache but appGraph couldn’t access ImageLoader.

You can also pass an Component Builder block to the createGraph or openSubgraph method to add new dependencies to the resulting subgraph.

Retrieving Dependencies

Dependencies are retrieved from a dependency graph.

val coffeeAppComponent = component {
    prototype { Heater() }

    prototype { RotaryPump() }

    singleton { CoffeeMaker(instance(), instance()) }
}

val graph = coffeeAppComponent.createGraph()

// get an instance of Heater
val heater: Heater = graph.instance()

// get an optional instance of Heater
val heater: Heater? = graph.instanceOrNull()

// get a provider for Heater
val heaterProvider: () -> Heater = graph.provider()

// get an optional provider for Heater
val heaterProvider: (() -> Heater)? = graph.providerOrNull()

// get a set of instances of type Pump; this is useful when you have registered
// multiple Pumps with different qualifers
val pumps: Set<Pump> = graph.instancesOfType<Pump>()

// get a set of providers for type Pump; this is useful when you have registerd
// multiple Pumps with different qualifers
val pumps: Set<() -> Pump> = graph.providersOfType<Pump>()

Like the scope methods we used to declare our dependencies all the retrieval functions take an optional qualifier for cases where we have the same type registered with different qualifiers (except the *OfType methods) and they all take an boolean to enable generic type preservation.

See the Graph API docs for further details.

Generics

By default all generics you pass to one of the scope methods or retrieval methods fall victim to type erasure which means for example List<Pump> becomes just List. It is possible to preserve the generic type information but since it is a little bit more expensive to do, it is not enabled by default.

All Component Builder scope methods and all instance retrieval methods take an optional generics boolean argument (which is false by default) to enable generic type preservation.

When you register a type with generics = true then you have to set generics = true when you retrieve that type.
val appComponent = component {
    singleton<Collection<TrackingBackend>>(generics = true) {
        listOf(FirebaseTracker(), MixpanelTracker())
    }
    singleton { ScreenTracker(backends = instance(generics = true)) }
}

Winter Injection Adapter

Sometimes we cannot use constructor injection because a framework may create an instance of a class for use. But we don’t want knowledge of how to create or retrieve a dependency graph in our classes and therefor Winters injection adapter system was created. The actual strategy to create, get and close a graph is part of an adapter.

Here is a basic example with the SimpleAndroidInjectionAdapter from the winter-androidx module that requires an "activity" subcomponent:

class MyApplication : Application() {
    override fun onCreate() {
        // declare application component
        Winter.component(ApplicationScope::class) {
            singleton<GitHubApi> { GitHubApiImpl() }

            singleton { RepoListViewModel(instance()) }

            subcomponent(ActivityScope::class) {
                singleton { Glide.with(instance<Activity>()) }
            }
        }

        /// Configure the injection adapter to use
        Winter.useSimpleAndroidAdapter()
        // Open the application graph
        Winter.inject(this)
    }
}

class MyActivity : Activity() {
    private val viewModel: RepoListViewModel by inject()
    private val glide: RequestManager by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        Winter.inject(this)
        super.onCreate(savedInstanceState)
    }
}
We call Winter.component here instead of just component which registers the component as the application component used by the Injection Adapters by default.

Injection Patterns

Constructor Injection

Constructor injection also called initializer injection is a pattern where all required dependencies are passed to the constructor. This way an instance is always initialized in a consistent state.

val coffeeAppComponent = component {
    singleton { Heater() }
    singleton<Pump> { RotaryPump() }
    singleton { CoffeeMaker(instance(), instance()) }
}

Property and Method Injection

Property or method injection is a pattern where dependencies are set on properties or passed to methods. This is the appropriate way when dependencies are optional or a class is from a third party and doesn’t offer an appropriate constructor.

val coffeeAppComponent = component {
    singleton { Heater() }
    singleton<Pump> { RotaryPump() }
    singleton {
        val coffeeMaker = CoffeeMaker()
        coffeeMaker.heater = instance()
        coffeeMaker.pump = instance()
        coffeeMaker
    }
}

Another way is to use the postConstruct callback instead of the factory block.

val coffeeAppComponent = component {
    singleton { Heater() }
    singleton<Pump> { RotaryPump() }
    singleton(
        postConstruct = {
            it.heater = instance()
            it.pump = instance()
        }
    ) { CoffeeMaker() }
}

Injection with Property Delegates

It is considered best practice to create all instances of your classes with a DI system and to have all dependencies injected via constructor or property injection by the DI system.

But sometimes this is not possible because instances of your classes are created by a framework like Android Activities and you need your classes to inject there dependencies themselves.

class MyActivity : Activity() {
    // eager injection of a non-optional dependency
    private val api: GitHubApi by inject()
    // eager injection of an optional dependency
    private val api: GitHubApi? by injectOrNull()
    // lazy injection of a non-optional dependency
    private val api: GitHubApi by injectLazy()
    // lazy injection of an optional dependency
    private val api: GitHubApi? by injectLazyOrNull()

    override fun onCreate(savedInstanceState: Bundle?) {
      // ... create or get the dependency graph
      Winter.inject(this)
      super.onCreate(savedInstanceState)
    }
}

This utilizes Kotlin property delegation and defers the dependency retrieval to a point in time were you are able to provide a dependency graph e.g. Activity#onCreate on Android.

In this example we see retrieval methods prefixed with lazy. Lazy injection means that the actual retrieval and therefore the actual instantiation of a dependency is deferred to the point where you access the property the first time. This is useful in cases where the creation is computationally expensive but may not be required in some cases.

For more details see Delegate API docs.

Android

Winter AndroidX Module

The winter-androidx module comes with two extendable injection Adapter, a WinterFragmentFactory to enable constructor injection in Fragments and a DependencyGraphContextWrapper to attach a different graph to an Android Context.

For example:

class MyActivity : AppCompatActivity() {

  private val myService by inject()

  override fun onCreate(savedInstanceState: Bundle?) {
    Winter.inject(this)
    super.onCreate(savedInstanceState)
  }

}

For more details see API docs.

Java Support

The winter-java module contains a class named JWinter that provides static methods to retrieve dependencies from a Graph.

For example:

// Retrieve an instance of String with the qualifier "a"
JWinter.instance(graph, String.class, "a");

For a list of all available methods see Winter Java.

RxJava2 Support

The winter-rxjava2 module contains a Winter Plugin that automatically disposes all singletons in a graph which implement Disposable.

To activate the plugin call Winter.installDisposablePlugin() before you instantiate any graph.

For more details see API docs.

JUnit Test

The JUnit4 and JUnit5 test support modules provide test extensions to hock into the graph lifecycle to extend the object graph of you class under test.

They offer the ability to automatically provide all mocks of your test class via the object graph and to inject dependencies from your object graph into your test class by using reflection.

Both modules use WinterTestSession under the hood and a configured by providing a WinterTestSession Builder block.

JUnit4

The JUnit4 module provides a JUnit4 TestRule that allows to extend the test graph to override dependencies of you class under test.

Example:

// Extend subgraph with subcomponent qualifier "presentation"
@get:Rule
val winterRule = WinterRule {
    extend(ApplicationScope::class) { // the component qualifier of the component we want to extend
        singleton<Dependency>(override = true) { TestDependency() }
    }

    testGraph(ApplicationScope::class) // the component qualifier of the graph we want to use
}

@Inject lateinit var classUnderTest: MyClassUnderTest

@Before
fun beforeEach() {
    Winter.openGraph()
}

JUnit5

The JUnit5 module provides a JUnit5 extension that allows to extend the test graph to override dependencies of you class under test.

Example:

// Extend subgraph with subcomponent qualifier "presentation"
@JvmField
@RegisterExtension
val winterExtension = WinterEachExtension {
    extend(ApplicationScope::class) {
        singleton<Dependency>(override = true) {  TestDependency() }
    }
}

@Inject lateinit var classUnderTest: MyClassUnderTest

@BeforeEach
fun beforeEach() {
    Winter.openGraph()
}

// JUnit5 also offers a ParameterResolver feature that we support to resolve dependencies from
// the test graph
fun my_test_method(@WInject dependency: Dependency) {
    // do something with dependency
}

Mocks & Spy

Whenever you use mocks to mock out certain dependencies of your class under test you have to setup your mocks and to somehow set or inject your mocked dependencies in the class under test.

Libraries like Mockito or EasyMock do an excellent job in creating mocks. Winter provides a nice solution to provide those mocks to your object graph to inject them into the class under test.

The Testing module that is used by the JUnit4 and JUnit5 enables us to automatically provide all properties that are annotated with Mock or Spy via the graph.

Example:

@get:Rule
val mockitoRule = MockitoJUnit.rule()

@get:Rule
val winterRule = WinterRule {
    // provide all mocks declared in MyTest in the application graph
    bindAllMocks()
}

@Mock lateinit var dependency1: MyDependency1

@Mock lateinit var dependency2: MyDependency2

@Inject lateinit var classUnderTest: MyClassUnderTest

@Before
fun beforeEach() {
    // create application object graph
    Winter.openGraph()
}

JSR330 Annotation Preprocessor

JSR-330 support is provided by the module winter-compiler.

The JSR-330 annotation preprocessor generates factories and members injectors for you classes that are annotated with JSR-330 annotations.

Configure Kapt

dependencies {
    kapt 'io.jentz.winter:winter-compiler:x.x.x'
}

Custom Scopes

A custom scope is created via an extended Scope annotation like:

package my.project.root.package.name.scope

import javax.inject.Scope

@Scope
@Retention
annotation class MyCustoScope

The Winter core module already provides a scope called ApplicationScope which is the default for all components. The Winter AndroidX modules also provides two scopes called ActivityScope and PresentationScope.

Every class that is annotated with a scope annotation will be registered as a singleton. Winter provides two annotations, EagerSingleton and Prototype to change that to a eager-singleton or prototype.

Here a simple example of our CoffeeMaker:

@ApplicationScope
@InjectConstuctor
class Pump

@ApplicationScope
@InjectConstuctor
class Heater

@ApplicationScope
@InjectConstuctor
class CoffeeMaker(val pump: Pump, val heater: Heater)

Winter.component {
    generated<Pump>()
    generated<Heater>()
    generated<CoffeeMaker>()
}
val coffeeMaker: CoffeeMakter = Winter.openGraph().instance()

Advanced Usage

Include Components

TODO

Circular Dependencies

Circular dependencies are dependencies that depend on each other. To define circular dependencies in Winter one of the dependencies must be injected through a property or method. You can then use a postConstruct callback to retrieve the circular dependency.

class Parent(child: Child)
class Child {
    var parent: Parent? = null
}

val applicationComponent = component {
    singleton { Parent(instance()) }
    singleton(postConstruct = { it.parent = instance() }) { Child() }
}

Plugins

TODO

Usage In Libraries

TODO