Winter is a lightweight dependency injection library for Android and the JVM written in Kotlin.
Quickstart - An Android sample
In this example we use the AndroidPresentationScopeInjectionAdapter
from the AndroidX module to
create a presentation scope that outlives Activity configuration changes (like rotations).
A presentation scope is perfect to store presenters or view models in.
-
We add Winter to our
build.gradle
file.
dependencies {
implementation 'io.jentz.winter:winter:x.x.x'
implementation 'io.jentz.winter:winter-androidx:x.x.x'
}
-
We declare our dependencies in our application class.
class MyApplication : Application() {
override fun onCreate() {
// The application component with all dependencies that are available system wide.
Winter.component(ApplicationScope::class) {
singleton<GitHubApi> { GitHubApiImpl() }
// The presentation subcomponent with dependencies which are available to
// activities/fragments/views but outlives configuration changes.
subcomponent(PresentationScope::class) {
singleton { RepoListPresenter(gitHubApi = instance()) }
// The activity subcomponent with dependencies which get destroyed with every
// configuration change.
subcomponent(ActivityScope::class) {
singleton { Glide.with(instance<Activity>()) }
}
}
}
// Configure the AndroidPresentationScopeAdapter as the injection adapter to use.
Winter.useAndroidPresentationScopeAdapter()
// Open the application graph.
Winter.inject(this)
}
}
-
Start injecting dependencies into your activity.
class MyActivity : AppCompatActivity() {
private val viewModel: RepoListViewModel by inject()
private val glide: RequestManager by inject()
override fun onCreate(savedInstanceState: Bundle?) {
// This line will request the graph for an Activity from the configured injection adapter
// and then uses the graph to inject the dependencies into the properties we declared
// above.
// The AndroidPresentationScopeAdapter we configured in our application class will create
// the presentation graph the first time this activity is created and will then create
// the activity subgraph every time this is called.
// It uses LifecycleObserver and ViewModel from Androids architectural components to
// close the activity graph when Activity#destroy is called and to close the presentation
// subgraph when the activity finishes for good.
Winter.inject(this)
super.onCreate(savedInstanceState)
}
}
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.9.0.
Available Modules
Module | Description | API Doc |
---|---|---|
io.jentz.winter:winter:0.9.0 |
Winter core module |
|
io.jentz.winter:winter-junit4:0.9.0 |
JUnit4 support |
|
io.jentz.winter:winter-junit5:0.9.0 |
JUnit5 support |
|
io.jentz.winter:winter-testing:0.9.0 |
Testing support shared between the JUnit4 and JUnit5 modules |
|
io.jentz.winter:winter-rxjava2:0.9.0 |
RxJava2 support |
|
io.jentz.winter:winter-androidx:0.9.0 |
Android X support |
|
io.jentz.winter:winter-java:0.9.0 |
Java support |
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.
Lifecycle Hooks
Each dependency provider (except constant) has an optional callback that is called after creating
a dependency and all its dependencies, the so called postConstruct
callback which can be useful
in cases where we have circular dependencies.
Singletons also have the onClose
callback which is called when the dependency graph gets closed.
This is particularly useful to free resources like closing an open connection or cancel jobs.
val dbComponent = component {
singleton(onClose = { it.close() }) {
DbConnection()
}
}
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 |
weakSingleton |
Like singleton but the instance is hold as a |
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 a 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 Adapters and a
DependencyGraphContextWrapper to attach a different graph to an Android Context.
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
on Graph#close()
.
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 the subgraph with subcomponent qualifier PresentationScope::class
@get:Rule
val winterRule = WinterRule {
extend(PresentationScope::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
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(val child: Child)
class Child {
lateinit var parent: Parent
}
val applicationComponent = component {
singleton { Parent(instance()) }
singleton(postConstruct = { it.parent = instance() }) { Child() }
}
Plugins
TODO
Usage In Libraries
TODO