As apps grow larger in scale, separation of classes by concern becomes increasingly important to keep an app maintainable and as this happens, classes will need to depend on each other.
It quickly comes to a point when it’s no longer practical to instantiate objects within the classes they are used, as you will also need to instantiate each and every single one of their dependencies.
This is the first episode of Holiday Android Training. Roll the intro!
Dagger and Koin are the two most renown dependency injection solutions for Android, with dagger being the more popular of the two, mainly because it’s a bit more robust.
But the ever present problem with Dagger is that there’s no definitive way on how to use it, proven by the fact that we have so many different versions of dagger which were a result of Google trying to figure out a single definitive way to use it (but still not ultimately coming up with an answer). Furthermore, the multiple versions of dagger only adds to the confusion.
So different apps will have their own dagger setups unique to them. And while Dagger can be a bit of pain to setup when initially creating the app, once it’s setup properly, it’s fairly easy to use.
This is further reflected in a developer’s skill level. A low mid or even a junior android developer usually won’t have any problems adding to a dagger dependency tree that’s already setup, but will encounter many problems if they tried to make their own dagger dependency tree all the way from its roots.
And there are various blog posts online that go through creating a dagger setup, although they can be helpful, most of them do still have problems that lead to an incomplete solution.
So this will be my attempt to once and for all, create the perfect dagger setup…. at least until next year when this becomes outdated but you know what let’s goo
Dagger Setup
So this setup is for an app in the MVVM architecture with a single activity that has multiple fragments but is designed in a way that it can scale just as easily if I want to add more activities. This is also modeled quite closely to the setup from the post written by Damilola Omoyiwola but with some of my own touches to make it more scaleable and fix some of the problems I found with it.
One thing I also want to get out of the way is that this setup makes use of Dagger Android which is now depracated for the newer Dagger Hilt. Hilt is still in alpha though, and although it looks solid, production use of it is still a controversial topic among many developers. I’m still yet to try it out myself but that will have to wait another time.
We start with the gradle setup. We only need to add the dagger dependencies to make this work.
In your app/build.gradle
file, ensure first that you have kapt enabled
plugins { ... id 'kotlin-kapt' }
Then add the Dagger under dependencies
dependencies { // Dagger implementation "com.google.dagger:dagger-android:$daggerVersion" implementation "com.google.dagger:dagger-android-support:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" kapt "com.google.dagger:dagger-android-processor:$daggerVersion" } daggerVersion = 2.30.1
I’m currently using dagger version 2.30.1
Project Structure
Pay your attention to the di and mvvm packages, as the rest are irrelevant to our setup. It’s a rather simple structure. You can setup the mvvm package however you want it as it doesn’t really matter. This setup does however rely a lot on using modules to segregate dependencies without much scoping at all, so I keep them all in a package. But at the end of the day, set it up however you want to as this makes no difference to the functionality of the setup. That’s all in the code.
Setting up the MVVM
Before we get injecting, let’s set up the components that depend on each other, those being our activities, fragments, and viewmodels.
class MainActivity : DaggerAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
Since I’m using fragments in this app, MainActivity here requires no change. However, if you want to make an app that handles the view in the activity rather than the fragment, simply apply the later setup for the fragment into the activity.
<!-- activity_main.xml --> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container_view" android:layout_width="match_parent" android:layout_height="0dp" android:name="androidx.navigation.fragment.NavHostFragment" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
activity_main.xml
to use a FragmentContainerView for hosting the fragments. This is why we don’t need any extra Kotlin code in MainActivity
<!-- nav_graph.xml --> <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/homeFragment"> <fragment android:id="@+id/homeFragment" android:name="com.ericdecanini.shopshopshoppinglist.mvvm.fragment.home.HomeFragment" android:label="HomeFragment" > </fragment> </navigation>
In the res/xml folder, create a file called nav_graph
where home_fragment
is automatically loaded into the view as the start destination. I’ll go deeper into fragments and navigation in another episode of HAT.
To reduce boilerplate with multiple fragments and viewmodels, create a BaseFragment class. This isn’t strictly needed, but I find it to be a nice layer of abstraction, and will set us up nicely for any common behaviour we want our fragments to follow.
abstract class BaseFragment<V: ViewModel>: DaggerFragment() { @Inject protected lateinit var viewModel: V }
@Inject
class HomeFragment : BaseFragment<HomeViewModel>() class HomeViewModel @Inject constructor() : ViewModel()
The next thing to do is create the concrete fragment and viewModel. At least one of each for now. With HomeFragment extending BaseFragment and passing in the generic type parameter of HomeViewModel, it will automatically injected once we finish the setup.
abstract class ViewModelFactory : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return create() as T } protected abstract fun create(): ViewModel }
di package, create the ViewModelFactory
@Singleton @Component( modules = [] ) interface AppComponent : AndroidInjector<ShopshopApplication> { @Component.Factory interface Factory { fun create(@BindsInstance application: Application): AppComponent } }
Proceed by creating the AppComponent
interface directly under the di package.
class ShopshopApplication: DaggerApplication() { override fun applicationInjector(): AndroidInjector<out DaggerApplication> { return DaggerAppComponent.factory().create(this) } }
DaggerAppComponent
, then directly under your app package, create an application class that extends DaggerApplication
<application android:name=".ShopshopApplication" ...
Don’t forget to declare your newly created application class in the manifest.
@Module( includes = [ AndroidSupportInjectionModule::class, ActivityBuildersModule::class ] ) class AppModule
Next, create AppModule
under the modules package. AndroidSupportInjectionModule
AppModule
.
@Singleton @Component( modules = [AppModule::class] ) interface AppComponent
AppComponent
and add AppModule
@Module abstract class ActivityBuildersModule { @ContributesAndroidInjector( modules = [MainModule::class] ) abstract fun contributeMainActivity(): MainActivity }
Next, create ActivityBuildersModule
under di/module/activity
@Module(includes = [MainFragmentsModule::class]) class MainModule
@Module abstract class MainFragmentsModule { @ContributesAndroidInjector(modules = [HomeModule::class]) abstract fun contributeHomeFragment(): HomeFragment }
Still under di/module/activity, create MainModule
and MainFragmentsModule
. MainModule
is a module tied only to MainActivity
Then MainFragmentsModule
is where you can contribute any fragments that will be used within MainActivity
and assign their own modules to them. This is kept separate from MainModule
@Module class HomeModule { @Provides internal fun provideViewModel( someString: String ): HomeViewModel = object: ViewModelFactory() { override fun create(): ViewModel = HomeViewModel( someString ) }.create(HomeViewModel::class.java) @Provides internal fun provideSomeString(): String = "Hello World" }
Finally, create the HomeModule
which is the module tied directly to our HomeFragment
, which makes use of the ViewModelFactory
class to create our ViewModel and provide it to our fragment which it receives via the @Inject
member variable in BaseFragment
.
I’ve seen other articles use @Binds
to provide the viewModel, along with a ViewModelKey, making use of Dagger Multibindings. The problem I encountered with this approach is that it’s rather difficult when you need any constructor parameters in the viewModel which is pretty much any app ever.
I left someString
above just to show an alternate way to provide the viewModel with constructor parameters. Of course later on, replace this with any parameters you actually will need, but for now, you can use someString
to test that the setup works.
Conclusion (& source code)
ActivityBuildersModule
), more fragments (i.e. MainFragmentsModule
Of course, if you need to add other services, you can follow standard practices such as creating a NetworkModule
class to provide all your Retrofit dependencies, then add that to AppModule
.
The source code can be found
I hoped this post helped and as always, happy coding ༼ つ ◕_◕ ༽つ