In this codelab, you will learn how to split an existing app into multiple modules.
Starting with a project containing a single app module,
we will multi-modularize the project like so:
You can download the source code needed for this codelab from the link below:
Alternatively, you can clone the GitHub repository by running the code below in the command line.
$ git clone https://github.com/DroidKaigi/architecture-components-samples.git
These are the branches we will use:
First, let's get familiar with the sample app. Open the app with Android Studio:
architecture-components-samples-droidkaigi-2020-codelab.zip
file, extract its contents.GithubBrowserSample
project with Android Studio.If your screen shows a GitHub login screen, please login. (If we don't, we'll hit the rate limit of GitHub API pretty soon)
If your device asks you to choose what app to open github.com with, choose Chrome and login to GitHub.
In this sample app, you can search for GitHub repositories.
Let's try searching "droidkaigi".
You should see the search results appear on your screen.
Let's give the api a new home.
Using the DroidKaigi/conference-app-2020 as a reference for directory structure, we will be adding modules such as "api" and "repository" to a data directory.
Let's start by making a data directory.
To make life easier, switch the Project view to Project Files.
Create the api module and set its package name to com.android.example.data.api
.
We need to declare this in the settings.gradle
as well. This change should build without errors.
include ':app',
':data:api'
Move AccessTokenParameter by drag & dropping it to data/api/src/main/java/com.android.example.data.api.
A dialog will appear; press Refactor.
The following problems will be detected, and Android Studio will confirm if you really want to continue.
For now, let's check the problems and press cancel.
So what caused the errors?
The root cause was that the data/api module doesn't know about com.google.gson.annotations.SerializedName which is being referenced.
To solve this, open the build.gradle in data/api and add dependencies.
Here, add the libraries the classes in the package com.android.example.github.api in the app module are dependant upon.
Referring the app/build.gradle file, rewrite the data/api/build.gradle like so:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.lifecycle.livedata_ktx
implementation deps.kotlin.stdlib
api deps.retrofit.runtime
implementation deps.retrofit.gson
implementation deps.timber
implementation deps.dagger.runtime
kapt deps.dagger.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Then, add a data/api module dependency to app/build.gradle.
The line with the add
comment is the line added. Don't forget to press Gradle Sync!
dependencies {
implementation project(":data:api") // add
implementation deps.app_compat
implementation deps.recyclerview
Just to make sure, check if the build succeeds.
Retry the move! This time it should succeed.
The same should be for ApiResponse, AccessTokenResponse, and GithubAuthService.
Check if the build succeeds after you move the classes. During migrations, you should check your builds frequently.
Some of you may have noticed, but the remaining classes in the api package cannot be migrated to the data/api module as-is.
This is because these classes depend on other classes in the app module.
In the next section, we will make the remaining classes multi-modularizable.
GithubService and RepoSearchResponse depend on classes in the com.android.example.github.vo package.
Here, we will add a model module that will be a place for common classes used for passing data between modules. This will remove the dependencies of GithubService and RepoSearchResponse and enable them to migrate to the data/api module.
Following the steps in the first split, add a model module in the project root.
Set its package name to com.android.example.model
.
Next, add libraries that the classes in com.android.example.github.vo depend on to model/build.gradle.
Edit model/build.gradle like so:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.kotlin.stdlib
implementation deps.retrofit.gson
implementation deps.room.runtime
kapt deps.room.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add dependencies to the model module in app module and api module.
dependencies {
implementation project(":model") // add
implementation project(":data:api")
dependencies {
api project(":model") // add
implementation deps.lifecycle.livedata_ktx
Classes other than RepoSearchResult can be migrated to the model module. Let's move them one by one.
We can't migrate RepoSearchResult because it depends on app module's GithubTypeConverters.
Move each class while paying attention to each of their dependencies.
This should make all classes except AuthenticationInterceptor migration-ready.
Let's look at the import statements in GithubService to check.
import androidx.lifecycle.LiveData
import com.android.example.data.api.ApiResponse
import com.android.example.model.Contributor
import com.android.example.model.Repo
import com.android.example.model.User
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
Besides public libraries such as LiveData and retrofit2, we can see that there only imports of the library module.
Migrate RepoSearchResponse and GithubService to the data/api module.
Building the app generates Data Binding compiler errors:
As it may be a bug in Android Studio's refactoring feature, variable types in the layout file are not suitably converted.
Let's fix the errors in the layout file.
Change this
<variable
name="searchResult"
type="com.android.example.model.Resource" />
to this
<variable
name="searchResult"
type="LiveData<Resource<List<Repo>>>" />
Change this
<variable
name="repo"
type="com.android.example.model.Resource" />
to this
<variable
name="repo"
type="LiveData<Resource<Repo>>>" />
Change this
<variable
name="user"
type="com.android.example.model.Resource" />
to this
<variable
name="user"
type="LiveData<Resource<User>>" />
As a result, AuthenticationInterceptor becomes the only api-related class that hasn't been migrated to the data/api module.
Since AuthenticationInterceptor depends on AccessTokenRepository, let's create a repository module.
Following the steps in the first split, create a repository module.
Set its package name to com.android.example.data.repository
.
Move the repository module to the data directory, and change its name in the settings.gradle.
include ':app',
':model',
':data:repository',
':data:api'
Since it will be difficult to migrate all of the classes at once, let's first migrate AccessTokenRepository which has a relatively small amount of dependencies.
First, edit data/repository/build.gradle. Then add a reference to the data/repository module from the app module.
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api project(":model")
implementation deps.core_ktx
implementation deps.lifecycle.livedata_ktx
implementation deps.kotlin.stdlib
implementation deps.dagger.runtime
kapt deps.dagger.compiler
implementation deps.kotpref.core
implementation deps.kotpref.initializer
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add a reference to the data/repository module from the app module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:repository") // add
implementation deps.app_compat
implementation deps.recyclerview
We can now migrate AccessTokenRepository to the data/repository module.
One would want to add api module references to the repository module and hide access to it, but unfortunately, we can't add circular references between modules.
We could think of a number of solutions, but in this case, let's create an api-builder module that builds GithubService and GithubAuthService, and migrate AuthenticationInterceptor there.
This way, we could prevent circular references between our modules.
Let's try it in our project.
Following the steps in the first split, create an api-builder module.
Set its package name to com.android.example.data.api_builder
.
Move the api-builder module to the data directory, and change its name in the settings.gradle.
include ':app',
':model',
':data:repository',
':data:api',
':data:api-builder'
Edit the data/api-builder/build.gradle file as follows:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api project(":data:api")
implementation project(":data:repository")
implementation deps.lifecycle.livedata_ktx
implementation deps.kotlin.stdlib
implementation deps.retrofit.runtime
implementation deps.retrofit.gson
api deps.okhttp_logging_interceptor
implementation deps.dagger.runtime
kapt deps.dagger.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Edit to make the app module depend on the data/api-builder module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder") // add
implementation project(":data:repository")
implementation deps.app_compat
implementation deps.recyclerview
Move AuthenticationInterceptor to the data/api-builder module.
Extract AppModule#provideGithubService and AppModule#provideGithubAuthService to a new class and migrate them to the data/api-builder module.
Here, we will create a class named ApiBuilder in the app module.
class ApiBuilder @Inject constructor(
private val authenticationInterceptor: AuthenticationInterceptor
) {
fun buildGithubService(
baseUrl: String,
loggingLevel: HttpLoggingInterceptor.Level
): GithubService {
val client = OkHttpClient.Builder()
.addNetworkInterceptor(HttpLoggingInterceptor().apply { level = loggingLevel })
.addInterceptor(authenticationInterceptor)
.build()
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.client(client)
.build()
.create(GithubService::class.java)
}
fun buildGithubAuthService(
baseUrl: String
): GithubAuthService {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubAuthService::class.java)
}
}
Rewrite AppModule to use ApiBuilder.
AppModule
@Singleton
@Provides
fun provideGithubService(
apiBuilder: ApiBuilder
): GithubService {
return apiBuilder.buildGithubService(
baseUrl = "https://api.github.com/",
loggingLevel = HttpLoggingInterceptor.Level.BODY
)
}
@Singleton
@Provides
fun provideGithubAuthService(
apiBuilder: ApiBuilder
): GithubAuthService {
return apiBuilder.buildGithubAuthService(
baseUrl = "https://github.com/"
)
}
Now, we can migrate
over to the api-builder module. Move them.
Next, we will migrate the remaining repository-related classes to the data/repository module. We will start by creating a data/db module as a module for depended db-related classes.
Following the steps in the first split, create an db module.
Set its package name to com.android.example.data.db
.
Move the repository module to the data directory, and change its name in the settings.gradle.
include ':app',
':model',
':data:db',
':data:repository',
':data:api',
':data:api-builder'
Next, we will make changes to the OpenForTesting annotation, which db-related classes and repository classes depend on, so that it can be referenced from other modules.
We will create a module for OpenForTesting, and add references in other modules.
Note that the implementation of OpenForTesting differs between release and debug, so we will make two different modules as well.
Following the steps in the first split, create a testing-release module and a testing-debug module.
Set both package names to com.android.example.testing
.
Edit both build.gradle files so that it looks like the following:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.kotlin.stdlib
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add dependencies to the testing-release module and the testing-debug module in each build.gradle file.
Also, add a dependency to the data/api module in the app module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db") // add
releaseImplementation project(":testing-release") // add
debugImplementation project(":testing-debug") // add
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api project(":model")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation deps.lifecycle.livedata_ktx
api deps.room.runtime
implementation deps.kotlin.stdlib
implementation deps.timber
kapt deps.room.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
dependencies {
api project(":model")
releaseImplementation project(":testing-release") // add
debugImplementation project(":testing-debug") // add
implementation project(":data:api") // add
implementation deps.core_ktx
implementation deps.lifecycle.livedata_ktx
implementation deps.kotlin.stdlib
Switch Build Variants to release, and move the OpenForTesting.kt from app/src/release to the testing-release module.
Note that Android Studio's refactoring features function only with the currently selected Build Variant.
Next, switch Build Variants back to debug and move the OpenForTesting.kt from app/src/debug to the testing-debug module.
Check if both release and debug Build Variants build successfully, just to make sure.
Move RepoSearchResult and all classes located under the com.android.example.github.db package to the db module.
What other classes do the repository classes depend on?
AppExecutors looks like the only remaining class. If we split this out to another module, we can migrate the repository classes to the data/repository module.
Create an executor module and migrate AppExecutors to it.
Following the steps in the first split, create a executor module.
Set its package name to com.android.example.executor
.
Add dependencies to both the repository module and the executor module.
dependencies {
api project(":model")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":data:api")
implementation project(":executor") // add
implementation deps.core_ktx
implementation deps.lifecycle.livedata_ktx
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":executor") // add
implementation deps.app_compat
To prepare for migrating AppExecutors to the executor module, edit the executor/build.gradle as follows:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.kotlin.stdlib
implementation deps.dagger.runtime
kapt deps.dagger.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Migrate AppExecutors to the executor module.
Migrate AbsentLiveData and RateLimiter, which the repository classes depend on, to the data/repository module first.
Then, make the data/repository module dependant on the data/db module.
dependencies {
api project(":model")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":data:api")
implementation project(":data:db") // add
implementation project(":executor")
implementation deps.core_ktx
implementation deps.lifecycle.livedata_ktx
Finally, migrate all classes under the com.android.example.github.repository package over to the repository module.
We have now split everything in the app module into the following library modules:
Until now, we have been splitting classes that don't include views.
Let's try modularizing the login screen.
Create a feature directory, in the same way we created the data directory.
Set the package name to com.android.example.feature.login
.
Move the login module over to the feature directory, and change its name in the settings.gradle.
include ':app',
':feature:login',
':executor',
':testing-release',
':testing-debug',
':model',
':data:db',
':data:repository',
':data:api',
':data:api-builder'
For the feature/login module, we will migrate LoginActivity and LoginHelper.
Keeping this in mind, edit the build.gradle file in the feature/login module as follows:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:repository")
implementation deps.app_compat
implementation deps.lifecycle.livedata_ktx
implementation deps.lifecycle.runtime_ktx
implementation deps.browser
implementation deps.kotlin.stdlib
implementation deps.coroutines.android
implementation deps.timber
implementation deps.dagger.runtime
kapt deps.dagger.compiler
kapt deps.lifecycle.compiler
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add a feature/login module dependency to the app module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":executor")
implementation project(":feature:login") // add
implementation deps.app_compat
implementation deps.recyclerview
LoginHelper references the following constants in BuildConfig.
However, the BuildConfig is declared in the app module, so it is unable to be referenced once LoginHelper is moved to the feature/login module.
Here, we will define an EnvVar interface to wrap BuildConfig and place it in an envvar module. The implementation of EnvVar will be done in the app module.
Create the module and set its package name to com.android.example.envvar
.
Edit the envvar/build.gradle file like so:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.kotlin.stdlib
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add an envvar module dependency to the feature/login module and the app module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:repository")
implementation project(":envvar") // add
implementation deps.app_compat
implementation deps.lifecycle.livedata_ktx
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":executor")
implementation project(":envvar") // add
implementation project(":feature:login")
implementation deps.app_compat
Next, add an EnvVar interface to the envvar module.
package com.android.example.envvar
interface EnvVar {
val GITHUB_CLIENT_ID: String
val GITHUB_CLIENT_SECRET: String
}
Implement and provide the EnvVar in the AppModule.
@Singleton
@Provides
fun provieEnvVar(): EnvVar {
return object : EnvVar {
override val GITHUB_CLIENT_ID = BuildConfig.GITHUB_CLIENT_ID
override val GITHUB_CLIENT_SECRET = BuildConfig.GITHUB_CLIENT_SECRET
}
}
Remove any direct references to BuildConfig in LoginHelper, and use EnvVar instead.
Import com.android.example.github.BuildConfig when referencing BuildConfig.
class LoginHelper @Inject constructor(
private val githubAuthService: GithubAuthService,
private val accessTokenRepository: AccessTokenRepository,
private val envVar: EnvVar
) {
fun generateAuthorizationUrl(): Uri =
Uri.Builder().apply {
scheme("https")
authority("github.com")
appendPath("login")
appendPath("oauth")
appendPath("authorize")
appendQueryParameter("client_id", envVar.GITHUB_CLIENT_ID)
}.build()
suspend fun handleAuthRedirect(intent: Intent): Boolean {
val uri = intent.data ?: return false
if (!uri.toString().startsWith("dgbs://login")) return false
val tempCode = uri.getQueryParameter("code") ?: return false
Timber.i("code: $tempCode")
val param = AccessTokenParameter(
clientId = envVar.GITHUB_CLIENT_ID,
clientSecret = envVar.GITHUB_CLIENT_SECRET,
code = tempCode
)
return runCatching {
val resp = githubAuthService.createAccessToken(param)
accessTokenRepository.save(AccessToken(resp.accessToken))
}.onFailure {
Timber.e(it, "createAccessToken failed!")
}.isSuccess
}
}
This should make LoginHelper migratable to the feature/login module. Move it.
Because LoginActivity is dependant on the interface Injectable, we are unable to migrate it out of the app module.
Let's create a di module and move Injectable there.
Create a di module and set its package nameはcom.android.example.di
.
Edit the di/build.gradle file like so:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation deps.kotlin.stdlib
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.espresso.core
testImplementation deps.junit
}
Add a di module dependency to the app module and the feature/login module.
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":executor")
implementation project(":envvar")
implementation project(":di") // add
implementation project(":feature:login")
implementation deps.app_compat
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:repository")
implementation project(":envvar")
implementation project(":di") // add
implementation deps.app_compat
Next, migrate Injectable to the di module.
With this, LoginActivity has finally become migratable to the feature/login module.
Move activity_login.xml and LoginActivity in this order to the feature/login module.
When you add a module, Android Studio adds an unnecessary kotlin-gradle-plugin dependency in the root build.gradle. Remove these.
The final build.gradle should look like this:
buildscript {
apply from: 'versions.gradle'
addRepos(repositories)
dependencies {
classpath deps.android_gradle_plugin
classpath deps.kotlin.plugin
classpath deps.kotlin.allopen
classpath deps.navigation.safe_args_plugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
addRepos(repositories)
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Remove any unnecessary dependencies in app/build.gradle as well. Some dependencies are only needed in their relevant library modules' build.gradle.
The final app/build.gradle should look like this:
dependencies {
implementation project(":model")
implementation project(":data:api-builder")
implementation project(":data:repository")
implementation project(":data:db")
releaseImplementation project(":testing-release")
debugImplementation project(":testing-debug")
implementation project(":executor")
implementation project(":envvar")
implementation project(":di")
implementation project(":feature:login")
implementation deps.app_compat
implementation deps.recyclerview
implementation deps.cardview
implementation deps.material
implementation deps.core_ktx
implementation deps.transition
implementation deps.navigation.fragment_ktx
implementation deps.lifecycle.livedata_ktx
implementation deps.lifecycle.runtime_ktx
implementation deps.lifecycle.java8
implementation deps.glide.runtime
implementation deps.dagger.runtime
implementation deps.dagger.android
implementation deps.dagger.android_support
implementation deps.constraint_layout
implementation deps.kotlin.stdlib
implementation deps.coroutines.android
implementation deps.timber
kapt deps.dagger.android_support_compiler
kapt deps.dagger.compiler
kapt deps.lifecycle.compiler
testImplementation deps.junit
testImplementation deps.mock_web_server
testImplementation deps.arch_core.testing
testImplementation deps.mockito.core
androidTestImplementation deps.atsl.core
androidTestImplementation deps.atsl.ext_junit
androidTestImplementation deps.atsl.runner
androidTestImplementation deps.atsl.rules
androidTestImplementation deps.app_compat
androidTestImplementation deps.recyclerview
androidTestImplementation deps.cardview
androidTestImplementation deps.material
androidTestImplementation deps.espresso.core
androidTestImplementation deps.espresso.contrib
androidTestImplementation deps.arch_core.testing
androidTestImplementation deps.mockito.core
androidTestImplementation deps.mockito.android
}
How did it go?
In this codelab, we looked at an app that started its development over three years ago, and split the app into multiple modules.
Due to this being a codelab, we have split the app into very small modules.
In practice, consolidating executor, envvar, and di into a more general core module may also a valid choice.
Giving a general name to a module, however, comes at the risk of becoming a "God" module containing all sorts of classes. You should choose the name carefully, and split the module when it starts becoming large.
Also, to prevent the codelab from becoming too lengthy, this codelab does not touch on strategies on Dagger in multi module environments.
Refer to apps like the DroidKaigi Conference app and try to enhance the Dagger-related code in the app.