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'
    // Android support
    implementation 'io.jentz.winter:winter-android:x.x.x'
    // AndroidX lifecycle support
    implementation 'io.jentz.winter:winter-androidx-lifecycle:x.x.x'
    // RxJava2 support module
    implementation 'io.jentz.winter:winter-rxjava2: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'
    // Optional JSR-330 support
    implementation 'javax.inject:javax.inject:1'
    kapt 'io.jentz.winter:winter-compiler:x.x.x'
}

// The optional JSR-330 support requires also a kapt configuration block like
kapt {
    arguments {
        // This tells the Winter compiler under which package name it should store
        // the generated component. See the JSR-330 section for more details.
        arg("winterGeneratedComponentPackage", "my.project.root.package.name")
    }
}

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

Available Modules

Module Description API Doc

io.jentz.winter:winter:0.5.0

Winter core module

Winter

io.jentz.winter:winter-junit4:0.5.0

JUnit4 support

Winter JUnit4

io.jentz.winter:winter-junit5:0.5.0

JUnit5 support

Winter JUnit5

io.jentz.winter:winter-testing:0.5.0

Testing support shared between the JUnit4 and JUnit5 modules

Winter Testing

io.jentz.winter:winter-rxjava2:0.5.0

RxJava2 support

Winter RxJava2

io.jentz.winter:winter-android:0.5.0

Android support

Winter Android

io.jentz.winter:winter-androidx-lifecycle:0.5.0

Android X lifecycle support

Winter AndroidX lifecycle support

Terminology

Term Definition

Component

The immutable dependency registry for providers, factories 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 ComponentBuilder 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()) }
    factory { color: Color -> Widget(instance(), color) }
}

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 ComponentBuilder.

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.

factory

This is like prototype but the factory block takes one argument.

multiton

This is like singleton but the factory block takes one argument and memorizes the return value for that argument. Every subsequent request with an equal argument will return the same instance.

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.init()
// open a subgraph
val guiGraph = appGraph.openSubgraph("gui")
// close a subgraph
appGraph.closeSubgraph("gui")
// or
guiGraph.dispose()

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

You can also pass an ComponentBuilder block to the openSubgraph method to add new dependencies to the resulting subgraph.

Retrieving Dependencies

Dependencies are retrieved from a dependency graph.

val coffeeAppComponent = component {
    prototype { Heater() }

    factory<Pump> { type: PumpType ->
        when(type) {
            PumpType.Thermosiphon -> Thermosiphon(instance())
            PumpType.Rotary -> RotaryPump()
        }
    }

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

val graph = coffeeAppComponent.init()

// 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 factory for Pump
val pumpFactory: (PumpType) -> Pump = graph.factory()

// get an optional factory for Pump
val pumpFactory: ((PumpType) -> Pump)? = graph.factoryOrNull()

// get an instance of Pump by providing an argument
val pump: Pump = graph.instance<PumpType, Pump>(PumpType.Rotary)

// get an optional instance of Pump by providing an argument
val pump: Pump? = graph.instanceOrNull<PumpType, Pump>(PumpType.Rotary)

// get a provider for Pump by providing an argument
val pumpProvider: () -> Pump = graph.provider<PumpType, Pump>(PumpType.Rotary)

// get an optional provider for Pump by providing an argument
val pumpProvider: (() -> Pump)? = graph.providerOrNull<PumpType, Pump>(PumpType.Rotary)

// get a set of instances of type Pump; this is useful when you have registerd
// 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 disabled by default.

All ComponentBuilder 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)) }
}

Injection

We don’t want knowledge of how to create or retrieve a dependency graph in our classes and therefor Injection was created. Injection allows us to create, get and dispose a dependency graph without having knowledge about the details. The actual strategy to create, get and dispose a graph is part of an adapter.

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

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

            singleton { RepoListViewModel(instance()) }

            subcomponent("activity") {
                singleton { Glide.with(instance<Activity>()) }
            }
        }

        /// Configure Injection to use the simple android adapter
        Injection.useSimpleAndroidAdapter()
        // Create application graph by providing the application instance
        Injection.createGraph(this)
    }
}

class MyActivity : Activity() {
    private val injector = Injector()
    private val viewModel: RepoListViewModel by injector.instance()
    private val glide: RequestManager by injector.instance()

    override fun onCreate(savedInstanceState: Bundle?) {
        Injection.createGraphAndInject(this, injector)
        super.onCreate(savedInstanceState)
    }

    override fun onDestroy() {
        Injection.disposeGraph(this)
        super.onDestroy()
    }

}

See "Injection API documentation" for more details.

We call Winter.component here instead of just component which registers the component as the application component used by the Injection Adapters by default.
When you use Injection#createGraph to create a graph you should always call Injection#disposeGraph to close it instead of directly calling #dispose on the resulting graph.

Injector

It is considered the best way to use constructor based injection to have a consistent state after initialisation and proper encapsulation. But sometime classes are instantiated by the system, like Activities on Android.

Then property injection is our only solution.

The usage of the Injector class is the recommended way to handle cases were you are not able to use constructor injection for your Kotlin classes.

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

Example:

class MyActivity : Activity() {

    private val injector = Injector()
    // eager injection of a non-optional dependency
    private val api: GitHubApi by injector.instance()
    // eager injection of an optional dependency
    private val api: GitHubApi? by injector.instanceOrNull()
    // lazy injection of a non-optional dependency
    private val api: GitHubApi by injector.lazyInstance()
    // lazy injection of an optional dependency
    private val api: GitHubApi? by injector.lazyInstanceOrNull()
    // injection of a non-optional factory
    private val factory: (Int) -> ProducedInstance by injector.factory()
    // injection of an optional factory
    private val factory: (Int) -> ProducedInstance by injector.factoryOrNull()

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

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 Injector API docs.

WinterAware

The WinterAware interface marks a class as aware of Winter and gives it access to a variety of extension methods to get a dependency graph and to retrieve or inject dependencies.

A simple example:

class HomeScreen @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : CoordinatorLayout(context, attrs, defStyleAttr), WinterAware {

  private val viewModel: HomeViewModel = instance()

}

The call to instance in this example is just syntactical sugar for Injection.getGraph(this).instance<HomeViewModel>().

For more details see the API documentation of WinterAware.

WinterTree & GraphRegistry

A object graph can have multiple subgraphs and may have a parent graph which makes it a tree of object graphs (directed acyclic graph).

WinterTree and its object version GraphRegisty are helper to create (open) and dispose (close) (sub-)graphs by paths of component qualifier.

This was inspired by [Toothpick](https://github.com/stephanenicolas/toothpick).

You can use GraphRegistry directly but it is usually a better approach to use the Injection abstraction and use WinterTree in an Adapter internally.

For example:

// create the application dependency graph on application start
class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    // define a component with one subcomponent
    Winter.component {
      subcomponent("activity") {
      }
    }

    GraphRegistry.open { constant<Application> { this@MyApplication } }
  }
}
// you can now retrieve the application dependency graph by calling
GraphRegistry.get()

// create and dispose a subgraph of the application graph
class MyActivity : Activity() {
  override fun onCreate() {
    super.onCreate()
    // initialize subcomponent with name "activity" and register it with identifier this
    GraphRegistry.open("activity", identifier = this) { constant<Activity>(this@MyActivity) }
  }

  override fun onDestroy() {
    super.onDestroy()
    // dispose the "activity" subgraph with identifier this
    GraphRegistry.close(this)
  }

}

If you close (dispose) a graph it will also close all registered subgraphs.

For more details see WinterTree API docs and GraphRegistry API docs.

Android

Winter Android Module

The winter-android module comes with two extendable base Adapters for the Injection system and a DependencyGraphContextWrapper to attach a different graph to an Android Context.

The SimpleAndroidInjectionAdapter manages an application dependency graph and an activity subgraph.

The AndroidPresentationScopeAdapter manages an application dependency graph, a presentation subgraph that outlives configuration changes and an activity subgraph.

The DependencyGraphContextWrapper can be used to attache a different graph to an Android Context than the one that is attached to the wrapped Context.

For example:

Injection.getGraph(myActivity) // => activityGraph
val viewGraph = activityGraph.openSubgraph("view")
val viewContext = DependencyGraphContextWrapper(myActivity, viewGraph)
val newView = LayoutInflater.from(viewContext).inflate(R.layout.view_list, containerView, false)
Injection.getGraph(newView) // => viewGraph
Injection.getGraph(newView.context) // => viewGraph

AndroidX Lifecycle Support

The winter-androidx-lifecycle module adds extensions to Graph and to LifecycleOwner to register a LifecycleObserver on a LifecycleOwner which automatically disposes the graph when the LifecycleOwner gets destroyed (or stopped).

For example:

class MyActivity : AppCompatActivity(), WinterAware {

  private val injector = Injector()
  // ... do something with injector ...

  override fun onCreate(savedInstanceState: Bundle?) {
    createGraphAndInject(injector) // this class is WinterAware so we get this extension method
    autoDisposeGraph() // no need to override onDestroy() to call Injection.disposeGraph(this)
    super.onCreate(savedInstanceState)
  }

}

// Or if you work with graphs directly

class MyClass : ALifecycleOwner() {

  fun someMethod() {
    val graph = myClassComponent.init()
    graph.autoDispose(this)
  }

}

For more details see API docs.

RxJava2 Support

The winter-rxjava2 modules 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.

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 = WinterJUnit4.rule("presentation") {
    singleton<Dependency>(override = true) { myTestDependency }
}

val myTestDependency = TestDependency()

@Inject lateinit var classUnderTest: MyClassUnderTest

@Before
fun beforeEach() {
    // create application object graph
    applicationComponent.createGraph()
    // inject class under test
    winterRule.inject(this)
}

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 = WinterJUnit5.extension("presentation") {
    singleton<Dependency>(override = true) { myTestDependency }
}

val myTestDependency = TestDependency()

@Inject lateinit var classUnderTest: MyClassUnderTest

@BeforeEach
fun beforeEach() {
    // create application object graph
    applicationComponent.createGraph()
    // inject class under test
    winterExtension.inject(this)
}

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 modules provides a ComponentBuilder extension 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 = WinterJUnit4.rule {
    // provide all mocks declared in MyTest in the application graph
    bindAllMocks(this@MyTest)
}

@Mock lateinit var dependency1: MyDependency1

@Mock lateinit var dependency2: MyDependency2

@Inject lateinit var classUnderTest: MyClassUnderTest

@Before
fun beforeEach() {
    // create application object graph
    applicationComponent.createGraph()
    // inject class under test
    winterRule.inject(this)
}

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() }
}

Injector with Property Delegates

The Injector uses property delegates to inject (strictly speaking retrieve) dependencies. This is often the best option for classes that are created by a framework like Android Activities.

class CoffeeActivity : Activity() {
    private val injector = Injector()
    private val coffeeMaker: CoffeeMaker by injector.instance()

    override fun onCreate(savedInstanceState: Bundle?) {
        injector.inject(getGraph())
        super.onCreate(savedInstanceState)
        // ...
    }

}

For more details see the paragraph about the Injector.

JSR330 Annotation Preprocessor

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

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

The JSR-330 annotation preprocessor generates factories for your classes that have an @Inject annotated constructor.

It generates a members-injector for each class that has @Inject annotated setters or fields.

And it generates a component containing all those factories and members-injectors to avoid the usage of reflection.

Configure Kapt

dependencies {
    implementation 'javax.inject:javax.inject:1'
    kapt 'io.jentz.winter:winter-compiler:x.x.x'
}

kapt {
    arguments {
        arg("winterGeneratedComponentPackage", "my.project.root.package.name")
    }
}

This will generate a component named generatedComponent in the configured package here my.project.root.package.name.

In a simple application that only relies on JSR-330 for injection this generatedComponent can directly be used as application component but it is usually included in another component.

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 ApplicationScope

Every class that is annotated with this will be registered in a subcomponent with the qualifier ApplicationScope::class as a singleton.

Here a simple example of our CoffeeMaker:

@ApplicationScope
class Pump @Inject constructor()

@ApplicationScope
class Heater @Inject constructor()

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

val applicationGraph = generatedComponent.subcomponent(ApplicationScope::class).init()
val coffeeMaker: CoffeeMakter = applicationGraph.instance()

Advanced Usage

Include Components

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

Usage In Libraries