I bring to your attention the translation of the original article from Jamie Sanson
Dependency Injection (DI) is a general model used for all reasons in all forms of development. Thanks to the Dagger project, it is taken as a template used in Android development. Recent changes in Android 9 Pie have led to the fact that we now have more options when it comes to DI, especially with the new AppComponentFactory
class.
DI is very important when it comes to modern Android development. This reduces the total amount of code when getting links to the services used between classes, and generally divides the application into components well. In this article, we will focus on Dagger 2, the most common DI library used in Android development. It is assumed that you already have basic knowledge of how this works, but it is not necessary to understand all the details. It is worth noting that this article is something like an adventure. This is interesting and everything, but at the time of its writing, the Android 9 Pie did not even appear on the platform’s version panel , so this topic will probably not be related to everyday development for at least several years.
Simply put, we use DI to provide instances of “dependency” classes to our dependent classes, that is, those that do the work. Let's assume that we use the Repository pattern to process our data-related logic, and want to use our repository in the Activity to display some data to the user. We may want to use the same repository in several places, so we use dependency injection to simplify sharing the same instance between a bunch of different classes.
To begin, we will provide a repository. We will define the Provides
function in a module that lets Dagger know that this is the instance that we want to implement. Please note that our repository needs an instance of the context for working with files and the network. We will provide him with the application context.
@Module class AppModule(val appContext: Context) { @Provides @ApplicationScope fun provideApplicationContext(): Context = appContext @Provides @ApplicationScope fun provideRepository(context: Context): Repository = Repository(context) }
Now we need to define Component
to handle the implementation of the classes in which we want to use our Repository
.
@ApplicationScope @Component(modules = [AppModule::class]) interface ApplicationComponent { fun inject(activity: MainActivity) }
Finally, we can configure our Activity
to use our repository. Suppose we created an instance of our ApplicationComponent
somewhere else.
class MainActivity: AppCompatActivity() { @Inject lateinit var repository: Repository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // application.applicationComponent.inject(this) // } }
That's all! We have just configured dependency injection within an application using Dagger. There are several ways to do this, but this seems like the simplest approach.
In the examples above, we saw two different types of injections, one more obvious than the other.
The first one you may have missed is known as embedding into the constructor . This is a method of providing dependencies through a class constructor, meaning that a class using dependencies has no idea about the origin of instances. This is considered the purest form of dependency injection, as it perfectly encapsulates our logic of embedding into our Module
classes. In our example, we used this approach to provide a repository:
fun provideRepository(context: Context): Repository = Repository(context)
For this, we needed a Context
, which we provided in the function provideApplicationContext()
.
The second, more obvious, that we saw, this introduction in the field of a class . This method was used in our MainActivity
to provide our repository. Here we define the fields as recipients of injections using the Inject
annotation. Then in our onCreate
function onCreate
we tell the ApplicationComponent
that we need to inject dependencies into our fields. It doesn’t look as clean as an introduction to the constructor, since we have an explicit reference to our component, which means that the concept of implementation leaks into our dependent classes. Another drawback in the Android Framework classes, since we need to be sure that the first thing we do is provide dependencies. If this happens at the wrong point in the life cycle, we may accidentally try to use an object that has not yet been initialized.
Ideally, you should completely get rid of intrusions into class fields. This approach misses information about the implementation of classes that do not need to know about it, and can potentially cause problems with life cycles. We have seen attempts to do this better, and Dagger in Android is a fairly reliable way, but in the end it would be better if we could just use the implementation implementation. Currently, we cannot use this approach for a number of framework classes, such as “Activity”, “Service”, “Application”, etc., since they are created for us by the system. It seems that at the moment we are stuck on introducing classes into fields. However, in Android 9 Pie, something interesting is being prepared that may change everything radically.
As mentioned at the beginning of the article, in Android 9 Pie there is an AppComponentFactory class. The documentation for it is rather scarce, and is posted simply on the developer’s site as such:
The interface used to control the creation of manifest elements.
It is intriguing. “Manifest elements” here refer to the classes that we list in our AndroidManifest
file — such as Activity, Service, and our Application class. This allows us to "control the creation" of these elements ... so wait a minute, can we now set the rules for creating our Activity? What a beauty!
Let's dig deeper. We will start by extending the AppComponentFactory
and overriding the instantiateActivity
method.
class InjectionComponentFactory: AppComponentFactory() { private val repository = NonContextRepository() override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return when { className == MainActivity::class.java.name -> MainActivity(repository) else -> super.instantiateActivity(cl, className, intent) } } }
Now we need to declare our component factory in the manifest inside the application tag.
<application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name=".InjectionApp" android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory" android:theme="@style/AppTheme" tools:replace="android:appComponentFactory">
Finally we can run our application ... and it works! Our NonContextRepository
provided through the MainActivity constructor. Gracefully!
Please note that there are some reservations. We cannot use Context
here, because even before its existence, a call to our function occurs - it is confusing! We can go further so that the constructor implements our Application class, but let's see how Dagger can make it even easier.
I will not go into the details of the work of Dagger multiple binding under the hood, as this is beyond the scope of this article. All you need to know is that it provides a good way to embed in a class constructor without having to call the constructor manually. We can use this to easily implement the framework classes in a scalable way. Let's see how it all adds up.
Let's first set up our Activity to understand what is where to go next.
class MainActivity @Inject constructor( private val repository: NonContextRepository ): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // } }
From this it is immediately apparent that there are almost no references to dependency injection. The only thing we see is the Inject
annotation Inject
front of the designer.
Now you need to change the component and the Dagger module:
@Component(modules = [ApplicationModule::class]) interface ApplicationComponent { fun inject(factory: InjectionComponentFactory) }
@Module(includes = [ComponentModule::class]) class ApplicationModule { @Provides fun provideRepository(): NonContextRepository = NonContextRepository() }
Nothing much has changed. Now we only need to implement our component factory, but how do we create our manifest elements? Here we need the ComponentModule
. Let's get a look:
@Module abstract class ComponentModule { @Binds @IntoMap @ComponentKey(MainActivity::class) abstract fun bindMainActivity(activity: MainActivity): Any @Binds abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey internal annotation class ComponentKey(val clazz: KClass<out Any>)
Yeah, well, just a few annotations. Here we associate our Activity
with a map, implement this map into our ComponentHelper
class, and provide this ComponentHelper
— all in two Binds
instructions. Dagger knows how to create an instance of our MainActivity
thanks to MainActivity
annotations so it can “bind” the provider to this class, automatically providing the dependencies we need for the designer. Our ComponentHelper
as follows.
class ComponentHelper @Inject constructor( private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>> ): ComponentInstanceHelper { @Suppress("UNCHECKED_CAST") override fun <T> resolve(className: String): T? = creators .filter { it.key.name == className } .values .firstOrNull() ?.get() as? T } interface InstanceComponentHelper { fun <T> resolve(className: String): T? }
Simply put, we now have a class map for suppliers for these classes. When we try to resolve a class by name, we simply find the provider for this class (if we have one), call it to get a new instance of this class, and return it.
Finally, we need to make changes to our AppComponentFactory
to use our new helper class.
class InjectionComponentFactory: AppComponentFactory() { @Inject lateinit var componentHelper: ComponentInstanceHelper init { DaggerApplicationComponent.create().inject(this) } override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return componentHelper .resolve<Activity>(className) ?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent) } }
Run the code again. It all works! What a beauty.
Such a headline may not look very impressive. Although we can embed most instances in the usual way by embedding in a constructor, we have no obvious way of providing context for our dependencies in standard ways. But the Context
in Android is all. It is needed to access the settings, network, application configuration, and more. Our dependencies are often things that use data-related services, such as the network and settings. We can get around this by rewriting our dependencies to consist of pure functions, or initializing everything with context instances in our Application
class, but it takes much more effort to determine the best way to do this.
Another disadvantage of this approach is the definition of scope. In Dagger, one of the key concepts for implementing high-performance dependency injection with a good separation of class relationships is the modularity of the object graph and the use of scope. Although this approach does not prohibit the use of modules, it limits the use of scope. AppComponentFactory
exists at a completely different level of abstraction relative to our standard framework classes — we cannot get a reference to it programmatically, so we have no way to instruct it to provide dependencies for an Activity
in another scope.
There are many ways to solve our problems with scopes in practice, one of which is to use the FragmentFactory
to embed our fragments in a scoped constructor. I will not go into details, but it turns out that now we have a method for managing the creation of fragments, which not only gives us much greater freedom in terms of scope, but also has backward compatibility.
In Android 9 Pie, there is a way to use implementation in the constructor to provide dependencies in our framework classes, such as “Activity” and “Application”. We saw that with Dagger Multi-binding, we can easily provide dependencies at the application level.
A designer that implements all our components is extremely attractive, and we can even do something to make it work properly with instances of the context. This is a promising future, but it is only available starting from API 28. If you want to reach less than 0.5% of users, you can try. Otherwise, it is worth waiting and see if this method will remain relevant in a few years.
Source: https://habr.com/ru/post/444530/
All Articles