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.
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?
Great Job Very helpful. Thanks for sharing with us valuable things.
Charles Johns