Minimalist and fast dependency injection library for Android.
Magnet is fully open source and available under Apache License, Version 2.0 at https://github.com/sergejsha/magnet.
It is provided for free, without any kind of support. If you consider using Magnet in your commercial product and you need additional support or training, feel free to contact me.
Why create another dependency injection library? Here are the objectives Magnet tries to pursue better that the others:
Simplicity. Magnet is based on just two simple concepts - scopes and instances. They are extremely easy-to-use and require almost no configuration (except for declarative annotations and creation of scopes).
Non-intrusive API. Your code should know very little to nothing about the injection framework it uses. Magnet tries to achieve this by generating helper code and transparently executing it.
Constructor Injection. Magnet enforces constructor injection. It requires declaration of a single constructor with dependencies, which improves encapsulation and protects the integrity of those dependencies at compile time.
Dependency inversion. When used in multi-module projects, Magnet is capable of injecting instances provided by any module, even if there is no build-time dependency between providing and consuming modules. This enforces clean and extensible software design.
Concise implementation. Magnet shrinks amount of supported features to the absolute minimum required. This helps to keep the method count to a minimum.
To use Magnet in your project, include it as a dependency into your build.gradle
file.
Kotlin
dependencies {
api 'de.halfbit:magnet-kotlin:2.3'
kapt 'de.halfbit:magnet-processor:2.3'
}
Java
dependencies {
api 'de.halfbit:magnet:2.3'
annotationProcessor 'de.halfbit:magnet-processor:2.3'
}
Releases of Magnet are published to mavenCentral()
repository.
Create an empty marker interface in your main application module and annotate it. If you have a single-module application, just add it to that module. This one-time initialization will trigger generation of all needed helper code.
@Magnetizer
interface AppMagnetizer
@Magnetizer
interface AppMagnetizer {}
Start wring classes needed for implementing your application and let Magnet know how to instantiate them by applying @Instance
annotation.
// Repository.kt
interface Repository {
fun getHelloMessage(): String
}
@Instance(type = Repository::class)
internal class DefaultRepository(): Repository {
override fun getHelloMessage() = "Hello Magnet!"
}
// Presenter.kt
@Instance(type = Presenter::class)
class Presenter(private val repository: Repository) {
fun presentHelloMessage() {
println(repository.getHelloMessage())
}
}
// Repository.java
interface Repository {
String getHelloMessage();
}
// DefaultRepository.java
@Instance(type = Repository.class)
class DefaultRepository implements Repository {
@Override
public String getHelloMessage() {
return "Hello Magnet!";
}
}
// Presenter.java
@Instance(type = Presenter.class)
class Presenter {
private Repository repository;
Presenter(Repository repository) {
this.repository = repository;
}
public void presentHelloMessage() {
System.out.println(repository.getHelloMessage());
}
}
In the example above we define a Repository
interface and its DefaultRepository
implementation. Annotation @Instance(type = Repository::class)
instructs Magnet to create an instance of DefaultRepository
when a new instance of Repository
type gets requested. Presenter
class declares a dependency of Repository
type. Its annotation @Instance(type = Presenter::class)
says, that Magnet needs to create new instance of Presenter
when a new instance of Presenter
type gets requested.
Now it’s time for glueing everything together and let Magnet creating and providing us with all instances needed. Magnet does it through a scope.
Create root scope and request instances of application classes.
val scope = Magnet.createRootScope()
val presenter: Presenter = root.getSingle()
presenter.presentHelloMessage()
Scope scope = Magnet.createRootScope();
Presenter presenter = scope.getSingle(Presenter.class);
presenter.presentHelloMessage();
Magnet will create instance of Presenter
and Repository
for you. Respecting the configuration above, Magnet will create an instance of DefaultRepository
and inject it into Presenter’s constructor automatically.
Magnet retains created instances in scopes. On other words a scope is a container for instances. Instance’s lifespan corresponds to the lifespan of the scope it belongs to. Thus, for example, if you have a scope which lives forever, all instances inside this scope will also live forever.
At first you need to create a scope. Newly created scope is empty and has no instances. Magnet adds instances to the scope as your application requests them. If you want to pre-fill the scope with some instances right away - those can be some system-provided instances like Context
in Android - then you can bind those instances into the scope programmatically.
val scope = createRootScope() {
bind<Context>(this@App)
bind(LayoutInflater.from(this@App))
}
Scope scope = Magnet.createRootScope()
.bind(Context.class, this)
.bind(LayoutInflater.class, LayoutInflater.from(this));
In the example above we create a root scope (the scope without a parent) and bind this
instance though Context
type and an instance of layout inflater through LayoutInflater
type. Now, when an instance of Context
type gets requested in the scope, Magnet will return this
instance back.
Once scope is created we can start requesting instances from it. Instances get requested from the scope by type. Type is a class or interface which requested instance must implement. This is the same type we used in @Instance
annotation and when we bound instances into the scope.
In the example below we request an instance of Presenter
type from the scope.
// main
val presenter: Presenter = scope.getSingle()
// Presenter.kt
interface Presenter {
fun present()
}
// main
Presenter presenter = scope.getSingle(Presenter.class);
// Presenter.java
interface Presenter {
void present();
}
When scope instance is requested, Magnet does the following:
@Instance
-annotated classes of requested type to create the instance and to bind it into the scope. Then the instance gets returned back.Code above requests an instance of Presenter
type, which has no implementation yet. If you execute this code, it will fail. Next sections describes how to let Magnet know which implementation of Presenter
type should be used, which an instance of Presenter
type is requested.
Annotating instance implementations is basically the main development effort related to usage of Magnet. By applying @Instance
annotation to implementation classes your instruct Magnet which class to use when instance of certain type is requested.
In the example below we implement Presenter
interface and annotate the implementation.
@Instance(type = Presenter::class)
internal class DefaultPresenter(
private val layoutInflater: LayoutInflater
) : Presenter {
override fun present() {
layoutInflater.inflate(...)
...
}
}
@Instance(type = Presenter.class)
class DefaultPresenter implements Presenter {
private final LayoutInflater layoutInflater;
DefaultPresenter(LayoutInflater layoutInflater) {
this.layoutInflater = layoutInflater;
}
@Override public void present() {
layoutInflater.inflate(...);
...
}
}
Once DefaultPresenter
class gets processed by Magnet’s annotation processor, Magnet will be able to instantiate it when scope instance of Presenter
type gets requested.
Sometimes you will need to instantiate third-party implementations which cannot be annotated by you. For this purposes Magnet supports annotation of static methods, which serve as instance factories. The example below demonstrates how you can provide an instance of SharedPreferences
class using a static method.
@Instance(type = SharedPreferences::class)
fun provideGlobalSharedPreferences(context: Context)
: SharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context)
class SharedPreferencesProvider() {
@Instance(type = SharedPreferences.class)
public static SharedPreferences provide(Context context) {
return PreferenceManager
.getDefaultSharedPreferences(context);
}
}
When type alone is not enough to differentiate between two implementations and you need a more fine-grained type classification, you can use classifier. Classifier is an additional differentiator inside the same type.
It can be used, for instance, to differentiate between Application and Activity contexts. Both implement the same Context
type, but after applying a classifier Magnet can easily provide one or another instance upon request.
// Activity.kt
const val ACTIVITY = "activity"
const val APPLICATION = "application"
// bind context
val activity = this
val scope = createRootScope() {
bind<Context>(activity, ACTIVITY)
bind<Context>(activity.applicationContext, APPLICATION)
}
// Renderer.kt
@Instance(type = Renderer::class)
internal class Renderer(
@Classifier(ACTIVITY) val activityContext: Context,
@Classifier(APPLICATION) val applicationContext: Context
)
At this point you might have been used to Kotlin syntax already. Thus all the other code snippets will be written in Kotlin only ;)
Magnet is capable of injecting instances in three different cardinalities: optional, single and many.
Optional cardinality corresponds to zero or one instance. If your app can handle a case, when instance cannot be provided by Magnet (e.g. there is no implementation available), then you should use this cardinality.
Request optional from scope:
val context: Context? = scope.getOptional<Context>()
Request optional though dependency:
@Instance(type = Renderer::class)
internal class Renderer(
val context: Context?
)
If Magnet cannot find an instance, it will inject null
. If however there are more than one instance found, Magnet will fail injection.
When used with Java, Magnet respects @Nullable
annotations if provided for enforced nullability.
Single cardinality corresponds to exactly one instance. This is the most commonly used cardinality. If your app requires exactly one instance of a type to be provided by Magnet, you should use this cardinality.
Request single from scope:
val context = scope.getSingle<Context>()
Request single though dependency:
@Instance(type = Renderer::class)
internal class Renderer(
val context: Context
)
If Magnet does not find requested instance or finds more than one instance, Magnet will fail injecting the value.
Many cardinality corresponds to zero or more instances. If you expect zero or more instances of certain type to co-exist in your app, then this is the cardinality of choice.
Request many from scope:
interface MenuItem
val menuItems = scope.getMany<MenuItem>()
Request many though dependency:
@Instance(type = Renderer::class)
internal class Renderer(
val menuItems: List<MenuItem>
)
The code above will inject all instances of MenuItem
available in app. For instance, if you write a new application module and declare a new implementation of MenuItem
for Magnet, new menu item will be available in the list right after the app gets recompiled and launched.
As you can see the cardinality uses List
as container for instances. This imposes some restrictions on injection of List
instances in Magnet. You cannot bind List
type into scopes. Neither can you provide instances of List
type. You should use custom classes instead. List
type is reserved for many cardinality.
The most power of Magnet can be gained by using hierarchical scopes. Each scope in Magnet can have an optional relation to a parent scope. If a parent relation is present, a scope hierarchy gets build up. The scope hierarchy can be any deep, but I suggest to keep it as flat as needed.
class App : Application() {
override fun onCreate() {
super.onCreate()
scope.apply {
bind(applicationContext, APP_CONTEXT)
bind(contentResolver)
}
}
companion object {
val scope: Scope = Magnet.createRootScope()
}
}
class PlayerActivity : Activity() {
private lateinit var scope: Scope
override fun onCreate(savedInstanceState: Bundle?) {
val activity = this
scope = App.scope.createSubscope {
bind<Resources>(activity.resources)
bind<Activity>(activity)
bind<LifecycleOwner>(activity)
bind(LayoutInflater.from(activity))
bind(act.supportFragmentManager)
}
}
}
Parent scope does never know it is a parent scope, because it does not hold any references to its children. Parent relation allows instances kept in a child scope accessing instances of its parent scope or even parent’s parent scope up to the root.
This leads us to the following observations:
Such design fits the nature of Android applications very well. For instance, we can distinguish the following Android application scopes and put them into a hierarchy:
Application scope gets created on application start and lives as long as out app lives. Activity scope is a child of Application scope. It gets created and destroyed as user navigates from one activity to another through the application. The same happens to the other scopes.
Instances retained in Activity scope can access instances retained in Application scope. For example a Presenter
from Activity scope can access NetworkManager
from Application scope.
Until now we learned how to bind instances into a scope. We also mentioned, that after Magnet creates a new instance it puts this instance into a scope. Scoping rules are exactly the rules Magnet applies when it puts instance into a scope.
After the instance is created, Magnet has to choose between the following options: retain the instance in scope or don’t retain. If first option is selected, there is another decision to make - in which exactly scope to retain it, because we have a scope hierarchy as you remember.
There is three scoping rules you can apply to instances for enforcing desired behavior.
Scoping.UNSCOPED
is used when Magnet has to create new instances each time when instance is requested. The behavior is similar to usage of standard instance factory - each time instance is requested, each time Magnet create a new instance.
@Instance(
type = PlayerViewModel::class,
scoping = Scoping.UNSCOPED
)
internal class PlayerViewModel : ViewModel()
Scoping.DIRECT
is used when Magnet has to bind created instance directly into the scope, from which this instance has been requested.
@Instance(
type = ViewHolderProvider::class,
scoping = Scoping.DIRECT
)
internal class SectionViewHolderProvider(
private val layoutInflater: LayoutInflater,
private val resources: Resources
) : ViewHolderProvider
Given the following scope hierarchy Fragment ➜ Activity ➜ App let’s see the behavoir of this scoping rule depending on initial and follow up requests.
Initially requesting from App scope. Magnet retains instance in App scope.
Initially requesting from Activity scope. Magnet retains instance in Activity scope.
Initially requesting from Fragment scope. Magnet retains instance in Fragment scope.
You should use Scoping.DIRECT
when you want to keep instances in scope, but you don’t want to ‘inject’ them to the parent scope, which is possible when the next scoping rule is used.
Scoping.TOPMOST
is the default scoping rule instructing Magnet to find the top most scope for the requested instance, where all dependencies of the instance can still be satisfied.
We have same Fragment ➜ Activity[Resources] ➜ App scope hierarchy, but this time Activity scope has an instance of Resources
bound into it.
Given the following classes, Magnet will inject UserInteractor
and Presenter
as following.
interface UserInteractor
@Instance(
type = UserInteractor::class,
scoping = Scoping.TOPMOST
)
internal class DefaultUserInteractor() : UserInteractor
@Instance(
type = Presenter::class,
scoping = Scoping.TOPMOST
)
class Presenter(
private val resources: Resources,
private val userInteractor: UserInteractor
)
Request Presenter
from Fragment scope. Resulting scope hierarchy looks like Fragment ➜ Activity[Resources, Presenter] ➜ App[DefaultUserInteractor]
DefaultUserInteractor
in App scope because DefaultUserInteractor
has no dependencies and its top most scope is the root scope of scope hierarchy.Presenter
in Activity scope, because is has two dependencies: Resources
and UserInteractor
. The top most scope where both dependencies are reachable is the Activity scope.Note: If we destroy Fragment scope and create a new Fragment2 scope then the hierarchy will look like this.
Fragment2 ➜ Activity[Resources, Presenter] ➜ App[DefaultUserInteractor].
Magnet keeps Activity and App scopes pre-filled with instances and another request of Presenter
instance from Fragment2 scope will return same instance Magnet has already created when initial Fragment scope existed.
Request Presenter
from Activity scope. The resulting scope hierarchy looks exactly like in case 1.
Request Presenter
from Application scope. Magnet will fail injecting, because Presenter
’s dependency on Resources
cannot be satisfied.
My implementation class has at least one optional dependency.
Scoping.DIRECT
or Scoping.UNSCOPED
would be safe options there because top most scoping might place your instance in different scopes depending on whether optional dependency instance is present or not.I want to inject fields into my activity (fragment, component etc.)
@Instance(
type = InstanceHolder::class,
scoping = Scoping.UNSCOPED
)
internal class InstanceHolder(
val imageLoader: ImageLoader,
val networkInteractor: NetworkInteractor
)
// MyActivity.kt
private lateinit var activityScope: Scope
private lateinit var holder: InstanceHolder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityScope = appScope.createSubscope()
holder = activityScope.getSingle()
}