Dagger2 / Muselee

Muselee 3: Dependency Injection

Muselee is a demo app which allows the user to browse popular music artists. It is not intended to be a fully-featured user app, but a vehicle to explore good app architecture, how to implement current best-practice, and explore how the two often go hand in hand. Moreover it will be used to explore how implementing some specific patterns can help to keep our app both maintainable, and easy to extend.

Previously we looked at some of the basics of our initial three module structure, with separate modules for core functionality, a topartists feature module, and the main app module. But actually wiring these up can take a little bit of work. First we need to ensure that we get the dependencies between our modules correct. core can have third-party dependencies, but should not depend on any other modules within the project; topartists can depend on core and other internal modules, but cannot depend on app; whereas app can depend on anything. While these rules are fairly straightforward to understand, they can impose some restrictions. However, if we putting our code within the correct modules, then we should be able to overcome any issues.

One thing that can help us to do this is using dependency injection, and for this project I am using Dagger 2 for this. The simple model that I will follow is that there will be a master ApplicationComponent which lives within the app module. This will be used to inject required object instances into their consumers. Both the object instances and the consumer injection will be managed by a modules within the feature modules themselves. So app will have a project dependency upon topartists which contains a module named TopArtistsModule. The ApplicationComponent in app therefore looks like this:

@Component(
    modules = [
        AndroidInjectionModule::class,
        ApplicationModule::class,
        TopArtistsModule::class
    ]
)
interface ApplicationComponent : AndroidInjector {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder()
}

I have opted to use AndroidInjector to do all of the heavy lifting for us, and we’ll see how this can save us a lot of boilerplate code later on. The modules that this uses are AndroidInjectionModule which is a class from the dagger-android-support package that gives us much of the Android injection behaviour that we need; ApplicationModule which injects components defined within the app module itself and we’ll look at in a moment; Finally there is TopArtistsModule which comes from the topartists module and injects its components.

As we progress through this series, we’ll see how much value and flexibility we get from this quite set up.

The ApplicationComponent only knows how to inject a MuseleeApplication instance:

class MuseleeApplication : DaggerApplication() {

    override fun applicationInjector(): AndroidInjector =
        DaggerApplicationComponent.builder().create(this)
}

This extends dagger.android.support.DaggerApplication which gives us the necessary AndroidInjector behaviour without any additional work. It isn’t absolutely necessary to subclass DaggerApplication, but you’ll have to implement the HasSupportFragmentInjector interface within your Application class in order to use AndroidInjector. The applicationInjector() function creates an appropriate instance by calling the generated code for the ApplicationComponent that we defined earlier.

We can now create the ApplicationModule that we saw earlier:

@Module
abstract class ApplicationModule {

    @ContributesAndroidInjector
    abstract fun bindMuseleeActivity(): MuseleeActivity
}

The @ContributesAndroidInjector causes code to be generated which does the magic here. The behaviour that we get from this is that MuseleeActivity gets appropriately injected so that it is able to support provide an AndroidInjector for any Fragments that are hosted within it.

I have opted to go single-Activity for this project because it is a fairly simple app and I do not intend to put any significant code in the Activity itself. MuseleeActivity looks like this:

class MuseleeActivity : DaggerAppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
        supportFragmentManager.beginTransaction().apply {
            replace(R.id.main_fragment, TopArtistsFragment())
            commit()
        }
    }
}

The only things worthy of note here are that we extend DaggerAppCompatActivity which, once again, does all of the AndroidInjector wiring for us. For now we will manually add TopArtistFragment but we’ll revisit this later on in the series.

It’s worth noting at this point that the app module only touches the topartists module in two places. In ApplicationComponent we use the TopArtistsModule and in MuseleeActivity we create a TopArtistsFragment instance. Just these two references, and Dagger 2 doing all of the stuff in between keeps out touch points to a minimum, and this is why I have chosen this approach. It really simplifies things for us if we get it right.

Turning our attention to the topartists module, we can see that it it also uses @ContributesAndroidInjector to bind TopArtistsFragment instances:

@Module
abstract class TopArtistsModule {

    @ContributesAndroidInjector(
        modules = [NetworkModule::class]
    )
    abstract fun bindTopArtistsFragment(): TopArtistsFragment

}

For now we’ll just keep things simple, and inject a simple string to demonstrate this all working, and NetworkModule is responsible for providing this:

@Module
class NetworkModule {

    @Provides
    fun testString() = "Hello World!"
}

Finally we come to TopAtristFragment. Once again we’ll just implement a placeholder for now:

class TopArtistsFragment : DaggerFragment() {

    @Inject
    lateinit var testString: String

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
        inflater.inflate(R.layout.fragment_top_artists, container, false).also {
            it.findViewById(R.id.test_text).text = testString
        }
}

The key things here are that we extend dagger.android.support.DaggerFragment which does all of the AndroidInjector wiring. We just need to annotate the fields which require injection and AndroidInjector does the rest.

For now, all we do here is to set the text of a TextView to the string that was injected.

If we run this, we can see that the injected string gets displayed within the Fragment:

Some Dagger methodologies may require us to access our MuseleeApplication to set up the injection within our Fragment and this simply would not be possible with the module structure that we have because that would create a circular dependency between app and topartists modules; so we’d need to move MuseleeApplication in to our core module, and we’ve already discussed how we want to keep this as lean as possible.

The approach that I have used gives us a nice way of making the feature modules responsible for their internal responsibilities, and exposing this in a way that can be cleanly included in the app module.

Before we finish, it is worth mentioning that I am using Jetpack / AndroidX components throughout Muselee. Jetifier is enabled by default on new projects created in Android Studio 3.2 and later projects. Even though Dagger 2 does not support the AndroidX Fragment implementation, Jetifier makes the necessary conversion for us so, provided we use the versions of DaggerFragment, DaggerAppCompatActivity, and DaggerApplication from the dagger.android.support module / package, then Jetifier will make the necessary alterations to make Dagger 2 use the AndroidX Fragment implementation rather than the legacy support library version with which it is implemented.

In the next article, we’ll take a look at some further optimisations that we can make to our build scripts to make them easier to maintain.

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. Interesting to see that your app module has a dependency on the features whereas when using dynamic.features (still in Beta) the features have a dependency on the app module.

    Do you have planns to look at dynamic features?

Leave a Reply to Charles Johns Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.