Multi-Module Apps with Kotlin and Dagger
In this blog; we’ll go over the multi-module application architecture in Android; as well as its integration with Dagger and some good code guidelines.
Working on a large application can be a tedious task; especially when you’re working in a large team. Merge conflicts, long compile times and bugs are just some of the problems introduced when your app grows in scale and you don’t modify its architecture accordingly.
By default, Android Studio creates a single module application, which tends to be alright for most use-cases. In a multi-module application, this module would be your main module, or your application module. This is the top-level module that depends on all the other modules we’ll be creating. Let’s say that you want to create an app that shows some information grabbed from the network and displays it on the UI. We’ll create several modules to fit our scenario and show you how to best utilize the power of spreading out your code over multiple modules.
First of all, let’s create our app by selecting Empty Compose Activity in Android Studio’s new project menu. Using a Compose activity isn’t a necessity if you’re not comfortable using Jetpack Compose yet, but I promise you, it’s easier than XML and ViewBindings.
If you followed the previous step, you’ll see something like this:
So, Android studio created an app module for us. Since we want a multi-module application, we’re going to create a few more modules. Android Studio makes this easy; just right-click your project root, and select New > Module, and you’ll end up with a screen like this
This is where our first obstacle appears. What are all those options, and which one do you want?
Well, this all depends on your needs. In order to start from scratch, I usually select Kotlin Library. This generates the package structure, a single class, and nothing else. For instance, the Android Library option would generate an Android Manifest as well, whereas the Phone & Tablet option would pop the same dialog that opens when you create an Android Studio project, with all the following options.
So, we select Kotlin Library and call our library lib-domain. In this module, we just define the interfaces for our data-fetching implementations which are going to be exposed to other modules. This way, our main module won’t have any idea what happens in our implementation of the networking requests, it’ll just see what we want it to see.
If you followed along up to here; you’ve ended up with this project structure:
The shortcut Shift + F6 will let you refactor the name of MyClass to something we might actually use; let’s say NetworkService.
We’re also going to change the type of NetworkService from a Class to an Interface, and add a suspend fun getInfoFromNetwork() : String. You should end up with your file looking like this
This is an interface; meaning we have to actually implement what we want to happen when you call the getInfoFromNetwork function.
Let’s create a new module; the same way as the previous one, and call it lib-data.
This time, we’ll rename the MyClass to NetworkServiceImpl, and have it implement the NetworkService module. If you’re seeing the following error, that means you’re on the right path.
Follow android studio and allow it to add the dependency for you. What this did is added a dependency in your lib-data build.gradle for the lib-domain module; and if you go in the module-level build.gradle, you’ll see the following:
This means that now our lib-data depends on lib-domain, and can use the classes defined within. Neat, right?
For my implementation of getInfoFromNetwork(), I’m just going to have the coroutine wait for a bit and then return a String as a way to simulate a long-running network request.
For this, however, our lib-data must depend on coroutines; for the delay function.
So let’s add
To our lib-data build.gradle
That’s it for our data handling part of the app!
Now, let’s move on to the presentation module. This is a module that contains our ViewModels and the other classes related to the presentation logic. Here we’ll do the necessary transformations of data to the models our UI uses, and this is the module that our UI will directly utilize.
Now, this module will have to be an Android library. We’re creating our ViewModel here, and that’s something from the Android library. We’re gonna need a manifest etc; so let’s let the android studio create that for us. Just pick the Android Library from the New Module selection screen and you’re good to go. Create a class named MainViewModel and add the
Dependency to your build.gradle (if you don’t get the import handled automatically).
Now we’re all good with our modules, but how will we hook up all of those modules? That’s where Dagger/Hilt comes into play.
Dagger is a dependency injection library that is kind of hard to use with Android components; however, Hilt’s built on top of Dagger and allows for much easier use.
Setting up Hilt takes a few steps, but it's fairly easy. What we need to do is add the following dependency to our root build.gradle
Then, in the app module, we need to add the Kotlin Annotation Processor plugin as well as the Hilt plugin
And lastly, declare our dependencies in the app build.gradle
With this, we set up hilt to work in our main app library. However, we’re going to need both of these in the lib-presentation as well, in order to utilize Hilts’ ViewModel assisted injection.
Override the Application class and annotate it with @HiltAndroidApp so it triggers code generation. Don’t worry, just one more step to go!
Create an ApplicationModule class in your app module, and make it look something like this
This is a ‘map’ that tells Hilt how to create our dependencies.
Now Hilt’s all set up and we can move on to our Activity and see how little code we need to actually hook everything up!
Add the @AndroidEntryPoint annotation on top of your activity class and use a
Delegate to grab the ViewModel
Add the imports and you’re done! Everything is hooked up and ready to go!
A bit of a teaser for Compose and state handling with Compose
Let’s create a sealed class in our MainViewModel and call it NetworkState;
We’ll have two states for this; Loading and Done.
We’re going to pass this to our Activity through Flows.
Add the coroutine dependency to your ViewModel and update it so it looks like this
Update your MainActivity class to call the getInfo method when it instantiates, and make Compose observe the state. Now you have a UI that updates based on the state it receives and gets absolutely no information about how the info is getting there! Beautiful, right?
In my next blog I’ll go more in-depth about Jetpack Compose and State Handling, in case you’re interested.
Subscribe to our newsletter
We send bi-weekly blogs on design, technology and business topics.