Creating a maintainable, flexible codebase is not easy but is an essential part of software engineering. In this series we’ll take a look at a simple, functional weather app and look at some of the issues in its design. We shall then refactor and re-design it to create a codebase which will be easier to maintain, less prone to bugs, and easier to add features to. This series is not going to be a deep dive in to the techniques and technologies that we’re going to use, but will be more an exploration of what benefits they give us. In this article we’ll look at dependency injection.



Previously we did a refactor of our app to move the logic for obtaining both the location and the current weather data out of our Fragment. However there were still a couple of issues. Firstly the Fragment was still manually constructing instances of the concrete implementation of both these providers and, secondly, there was a monster of a constructor in the OpenWeatherMapProvider class.

The issue of constructing the concrete instances of both LocationProvider and CurrentWeatherProvider could apparently be fixed quite easily. We could create a Factory class which is responsible for building these:

DependencyFactory class DependencyFactory { fun createCurrentWeatherProvider(context: Context): CurrentWeatherProvider = OpenWeatherMapProvider(context, BuildConfig.API_KEY) fun createLocationProvider(context: Context): LocationProvider = FusedLocationProvider(context) } 1 2 3 4 5 6 7 class DependencyFactory { fun createCurrentWeatherProvider ( context : Context ) : CurrentWeatherProvider = OpenWeatherMapProvider ( context , BuildConfig . API_KEY ) fun createLocationProvider ( context : Context ) : LocationProvider = FusedLocationProvider ( context ) }

This pattern is commonly known as a Service Locator and is used to abstract away the details of how to instantiate collaborators. While this removes the dependency on the concrete implementations from our Fragment, we still need to create the DependencyFactory instance within the Fragment itself which makes it difficult to use an alternative implementation which is capable of returning mock objects within our test code.

The monstrous constructor (which we’ll call ‘Frankonstructor’) is the result the combination of making OpenWeatherMapProvider testable, and the dependency structure that our Retrofit instance has:

net/OpenWeatherMapProvider.kt class OpenWeatherMapProvider( context: Context, private val appId: String, okHttpClient: OkHttpClient = OkHttpClient.Builder() .cache(Cache(context.cacheDir, cacheSize)) .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build(), converterFactory: Converter.Factory = MoshiConverterFactory.create( Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() ), retrofit: Retrofit = Retrofit.Builder() .client(okHttpClient) .baseUrl("https://api.openweathermap.org/") .addConverterFactory(converterFactory) .build(), private val service: OpenWeatherMap = retrofit.create(OpenWeatherMap::class.java), private val calls: MutableList<Call<Current>> = mutableListOf() ) : CurrentWeatherProvider { ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class OpenWeatherMapProvider ( context : Context , private val appId : String , okHttpClient : OkHttpClient = OkHttpClient . Builder ( ) . cache ( Cache ( context . cacheDir , cacheSize ) ) . addInterceptor ( HttpLoggingInterceptor ( ) . apply { level = HttpLoggingInterceptor . Level . BODY } ) . build ( ) , converterFactory : Converter . Factory = MoshiConverterFactory . create ( Moshi . Builder ( ) . add ( KotlinJsonAdapterFactory ( ) ) . build ( ) ) , retrofit : Retrofit = Retrofit . Builder ( ) . client ( okHttpClient ) . baseUrl ( "https://api.openweathermap.org/" ) . addConverterFactory ( converterFactory ) . build ( ) , private val service : OpenWeatherMap = retrofit . create ( OpenWeatherMap : : class . java ) , private val calls : MutableList < Call < Current > > = mutableListOf ( ) ) : CurrentWeatherProvider { . . . }

We can are using Kotlin default constructor arguments to create a Retrofit instance which will be used by default. However for our test we are able to specify an alternate Retrofit instance which means that we can use a mock in these cases. So while this approach improves out testability, that comes at a cost in terms of the understandability of our code, and our production code being polluted in this way.

The technique that we’re using here can be quite a useful one in many cases but, in this one, the complexity of instantiating our Retrofit instance makes it less practical. Obtaining instances of collaborators via constructor arguments is quite a useful thing and is commonly known as constructor injection. This might appear to be a good way of solving the other problem that we have: Use constructor injection to provide an instance of DependencyFactory to our Fragment. The problem here is that we are not responsible for creating instances of our Fragment classes. While we may do initially, in the event of a configuration change (such as a device rotation) the default behaviour is that the Android framework will destroy our Fragment and create a new instance of it by invoking the default constructor. So we really cannot use constructor injection for things such as Activities & Fragments which may be created by the framework.

One way that we can overcome all of these issues is to use a Dependency Injection library – in this case we’ll use Dagger 2. We’re not going to do a deep-dive in to Dagger 2 (there are plenty of other resources from which to learn that), but look more at some tricks for making things easier, and seeing the benefits that it can provide.

Let’s first look at the Dagger Module that will construct our CurrentWeatherProvider instance:

net/WeatherModule.kt @Module class WeatherModule { private val cacheSize: Long = 10 * 1024 * 1024 @Provides @Singleton fun providesOkHttpClient(context: Context) : OkHttpClient = OkHttpClient.Builder() .cache(Cache(context.cacheDir, cacheSize)) .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build() @Provides @Singleton fun providesConverterFactory(): Converter.Factory = MoshiConverterFactory.create( Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() ) @Provides @Singleton fun providesRetrofit(okHttpClient: OkHttpClient, converterFactory: Converter.Factory): Retrofit = Retrofit.Builder() .client(okHttpClient) .baseUrl("https://api.openweathermap.org/") .addConverterFactory(converterFactory) .build() @Provides @Singleton fun providesOpenWeatherMap(retrofit: Retrofit): OpenWeatherMap = retrofit.create(OpenWeatherMap::class.java) @Provides @Singleton fun providesCurrentWeatherProvider(service: OpenWeatherMap): CurrentWeatherProvider = OpenWeatherMapProvider(service, BuildConfig.API_KEY) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @ Module class WeatherModule { private val cacheSize : Long = 10 * 1024 * 1024 @ Provides @ Singleton fun providesOkHttpClient ( context : Context ) : OkHttpClient = OkHttpClient . Builder ( ) . cache ( Cache ( context . cacheDir , cacheSize ) ) . addInterceptor ( HttpLoggingInterceptor ( ) . apply { level = HttpLoggingInterceptor . Level . BODY } ) . build ( ) @ Provides @ Singleton fun providesConverterFactory ( ) : Converter . Factory = MoshiConverterFactory . create ( Moshi . Builder ( ) . add ( KotlinJsonAdapterFactory ( ) ) . build ( ) ) @ Provides @ Singleton fun providesRetrofit ( okHttpClient : OkHttpClient , converterFactory : Converter . Factory ) : Retrofit = Retrofit . Builder ( ) . client ( okHttpClient ) . baseUrl ( "https://api.openweathermap.org/" ) . addConverterFactory ( converterFactory ) . build ( ) @ Provides @ Singleton fun providesOpenWeatherMap ( retrofit : Retrofit ) : OpenWeatherMap = retrofit . create ( OpenWeatherMap : : class . java ) @ Provides @ Singleton fun providesCurrentWeatherProvider ( service : OpenWeatherMap ) : CurrentWeatherProvider = OpenWeatherMapProvider ( service , BuildConfig . API_KEY ) }

Hopefully you’ll agree that this code is pretty easy to understand. Each function is responsible for creating a different kind of object. The final method is what is responsible for constructing our CurrentWeatherProvider instance, and nicely hides the fact that it is actually creating an instance of OpenWeatherMapProvider. This function takes a single OpenWeatherMap argument which is required to create the instance. This is where Dagger does its magic: It is able to build the complete dependency graph back from this. It knows that it can obtain OpenWeatherMap instance by calling the providesOpenWeatherMap() function with the argument of a Retrofit instance; it then knows that it can obtain a Retrofit instance by calling providesRetrofit() ; and so on…

This now means that we can replace Frankonstructor with something far easier to understand:

net/OpenWeatherMapProvider.kt class OpenWeatherMapProvider( private val service: OpenWeatherMap, private val appId: String, private val calls: MutableList<Call<Current>> = mutableListOf() ) : CurrentWeatherProvider { ... } 1 2 3 4 5 6 7 8 class OpenWeatherMapProvider ( private val service : OpenWeatherMap , private val appId : String , private val calls : MutableList < Call < Current > > = mutableListOf ( ) ) : CurrentWeatherProvider { . . . }

Moreover, the imports list for OpenWeatherMapProvider is also a lot cleaner now because it no longer needs visibility of OkHttp and Moshi as it is no longer responsible for creating these dependencies of Retrofit.

Now that Dagger is capable of creating an instance of CurrentWeatherProvider we no longer have to worry about using a Factory or Service Locator. Instead we can inject this directly in to our Fragment – we’ll cover how to do this in a test environment later in the series. Typically this can be a little messy for Android because we construct our Dagger component in the Application object, and then have to reference that wherever we want to perform injection, and have knowledge of which specific Dagger component is the correct one to inject the current Fragment. So we would typically need code like this in our Fragment:

(context.applicationContext as WeatherStationApplication).weatherStationComponent.inject(this) 1 ( context . applicationContext as WeatherStationApplication ) . weatherStationComponent . inject ( this )

To me this is a slightly broken form of Dependency Injection because the component that needs injecting actually requires knowledge of the DI model being used in order to be injected.

However, there is another way that we can make life easier by using some Android extensions for Dagger. First we need to add the appropriate dependencies:

app/build.gradle dependencies { implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.0-alpha1' implementation 'androidx.preference:preference:1.0.0-alpha1' implementation 'androidx.constraintlayout:constraintlayout:1.1.0' implementation 'androidx.core:core-ktx:1.0.0-alpha1' implementation 'androidx.fragment:fragment-ktx:1.0.0-alpha1' implementation 'com.google.android.gms:play-services-location:15.0.1' implementation 'com.google.android.material:material:1.0.0-alpha1' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' implementation 'com.squareup.moshi:moshi-kotlin:1.6.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5' implementation 'com.google.dagger:dagger:2.16' implementation 'com.google.dagger:dagger-android-support:2.16' kapt 'com.google.dagger:dagger-compiler:2.16' kapt "com.google.dagger:dagger-android-processor:2.16" testImplementation 'junit:junit:4.12' testImplementation "com.nhaarman:mockito-kotlin:1.5.0" testImplementation 'io.kotlintest:kotlintest-runner-junit5:3.1.0' testImplementation 'org.threeten:threetenbp:1.3.3' testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.6.1' testImplementation junit5.unitTests() testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' androidTestImplementation 'androidx.test:runner:1.1.0-alpha3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3' } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.0-alpha1' implementation 'androidx.preference:preference:1.0.0-alpha1' implementation 'androidx.constraintlayout:constraintlayout:1.1.0' implementation 'androidx.core:core-ktx:1.0.0-alpha1' implementation 'androidx.fragment:fragment-ktx:1.0.0-alpha1' implementation 'com.google.android.gms:play-services-location:15.0.1' implementation 'com.google.android.material:material:1.0.0-alpha1' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' implementation 'com.squareup.moshi:moshi-kotlin:1.6.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5' implementation 'com.google.dagger:dagger:2.16' implementation 'com.google.dagger:dagger-android-support:2.16' kapt 'com.google.dagger:dagger-compiler:2.16' kapt "com.google.dagger:dagger-android-processor:2.16" testImplementation 'junit:junit:4.12' testImplementation "com.nhaarman:mockito-kotlin:1.5.0" testImplementation 'io.kotlintest:kotlintest-runner-junit5:3.1.0' testImplementation 'org.threeten:threetenbp:1.3.3' testImplementation group : 'org.slf4j' , name : 'slf4j-simple' , version : '1.6.1' testImplementation junit5 . unitTests ( ) testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0' androidTestImplementation 'androidx.test:runner:1.1.0-alpha3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3' }

Next we declare a Dagger Module which defines the Android components that we wish to inject. In this case it’s our CurrentWeatherFragment:

di/AndroidBuilder.kt @Module abstract class AndroidBuilder { @ContributesAndroidInjector abstract fun bindCurrentWeatherFragment(): CurrentWeatherFragment } 1 2 3 4 5 6 @ Module abstract class AndroidBuilder { @ ContributesAndroidInjector abstract fun bindCurrentWeatherFragment ( ) : CurrentWeatherFragment }

We then need to add this Module and AndroidInjectionModule (which is part of the library) as modules within our Dagger Component:

di/WeatherStationComponent.kt @Singleton @Component(modules = [ AndroidInjectionModule::class, AndroidBuilder::class, WeatherStationModule::class, LocationModule::class, WeatherModule::class ]) interface WeatherStationComponent { @Component.Builder interface Builder { @BindsInstance fun application(application: Application) : Builder fun build(): WeatherStationComponent } fun inject(application: WeatherStationApplication) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ Singleton @ Component ( modules = [ AndroidInjectionModule : : class , AndroidBuilder : : class , WeatherStationModule : : class , LocationModule : : class , WeatherModule : : class ] ) interface WeatherStationComponent { @ Component . Builder interface Builder { @ BindsInstance fun application ( application : Application ) : Builder fun build ( ) : WeatherStationComponent } fun inject ( application : WeatherStationApplication ) }

We now need to implement HasSupportFragmentInjector in our Application class:

WeatherStationApplication.kt class WeatherStationApplication : Application(), HasSupportFragmentInjector { @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> private val weatherStationComponent: WeatherStationComponent by lazy { DaggerWeatherStationComponent.builder() .application(this) .build() } override fun onCreate() { super.onCreate() weatherStationComponent.inject(this) AndroidThreeTen.init(this) } override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class WeatherStationApplication : Application ( ) , HasSupportFragmentInjector { @ Inject lateinit var fragmentDispatchingAndroidInjector : DispatchingAndroidInjector < Fragment > private val weatherStationComponent : WeatherStationComponent by lazy { DaggerWeatherStationComponent . builder ( ) . application ( this ) . build ( ) } override fun onCreate ( ) { super . onCreate ( ) weatherStationComponent . inject ( this ) AndroidThreeTen . init ( this ) } override fun supportFragmentInjector ( ) : AndroidInjector < Fragment > = fragmentDispatchingAndroidInjector }

Now our Fragment is easily injectable without having to reference our Application object, and know any specifics of the dependency modules:

CurrentWeatherFragment class CurrentWeatherFragment : Fragment() { @Inject lateinit var locationProvider: LocationProvider @Inject lateinit var currentWeatherProvider: CurrentWeatherProvider private lateinit var converter: Converter override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { inflater?.inflate(R.menu.main_menu, menu) super.onCreateOptionsMenu(menu, inflater) } override fun onCreate(savedInstanceState: Bundle?) { AndroidSupportInjection.inject(this) super.onCreate(savedInstanceState) } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class CurrentWeatherFragment : Fragment ( ) { @ Inject lateinit var locationProvider : LocationProvider @ Inject lateinit var currentWeatherProvider : CurrentWeatherProvider private lateinit var converter : Converter override fun onCreateOptionsMenu ( menu : Menu ? , inflater : MenuInflater ? ) { inflater ? . inflate ( R . menu . main_menu , menu ) super . onCreateOptionsMenu ( menu , inflater ) } override fun onCreate ( savedInstanceState : Bundle ? ) { AndroidSupportInjection . inject ( this ) super . onCreate ( savedInstanceState ) } . . . }

The key line here is line 14 – this performs the injection without requiring any direct access to the Application instance or any knowledge of the Dagger components. But, more importantly, we now no longer have to manually instantiate instances of FusedLocationProvider and OpenWeatherMapProvider, we get them injected in their more abstract form – lines 3-4.

So by using Dagger we have now managed to solve both of the issues we had at the start of this article: Frankonstructor is no longer causing us nightmares, and CurrentWeatherFragment now only has knowledge of the abstract interfaces for LocationProvider, and CurrentWeatherProvider.

But we’re not done yet. In the next article we’ll look at some issues that we still have which can easily be solved using the Android Architecture Components.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Related

Copyright © 2018 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.