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:

Pros of a Multi-Module Project

What You'll Learn

Prerequisites

What you'll need

Download the code

You can download the source code needed for this codelab from the link below:

Download zip

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:

  1. If you've downloaded the architecture-components-samples-droidkaigi-2020-codelab.zip file, extract its contents.
  2. Open the GithubBrowserSample project with Android Studio.
  3. Click the [Run] button, and choose an emulator or a connected device. Your device should display something like this:

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.

Making 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

Create the api module and set its package name to com.android.example.data.api.

Move the api module to the data directory

We need to declare this in the settings.gradle as well. This change should build without errors.

settings.gradle

include ':app',
        ':data:api'

Moving the AccessTokenParamater to the data/api module

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.

Root cause and solving them

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:

data/api/build.gradle

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!

app/build.gradle

dependencies {
    implementation project(":data:api") // add

    implementation deps.app_compat
    implementation deps.recyclerview

Just to make sure, check if the build succeeds.

Retry moving AccessTokenParameter to data/api

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.

What about the remaining classes?

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.

Adding the model 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.

Editing build.gradle

Next, add libraries that the classes in com.android.example.github.vo depend on to model/build.gradle.

Edit model/build.gradle like so:

model/build.gradle

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.

app/build.gradle

dependencies {
    implementation project(":model") // add
    implementation project(":data:api")

api/build.gradle

dependencies {
    api project(":model") // add

    implementation deps.lifecycle.livedata_ktx

Migrating to the model module

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.

Migration more classes to the data/api module

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.

Resolving Data Binding compiler errors

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.

Fixing search_fragment.xml

Change this

      <variable
            name="searchResult"
            type="com.android.example.model.Resource" />

to this

       <variable
            name="searchResult"
            type="LiveData&lt;Resource&lt;List&lt;Repo>>>" />

Fixing repo_fragment.xml

Change this

      <variable
            name="repo"
            type="com.android.example.model.Resource" />

to this

       <variable
            name="repo"
            type="LiveData&lt;Resource&lt;Repo>>>" />

Fixing user_fragment.xml

Change this

      <variable
            name="user"
            type="com.android.example.model.Resource" />

to this

       <variable
            name="user"
            type="LiveData&lt;Resource&lt;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.

settings.gradle

include ':app', 
        ':model', 
        ':data:repository',
        ':data:api'

Migrate AccessTokenRepository to the data/repository module

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.

data/repository/build.gradle

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.

app/build.gradle

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.

Where to migrate AuthenticationInterceptor?

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.

Create an api-builder module

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.

settings.gradle

include ':app', 
        ':model', 
        ':data:repository',
        ':data:api',
        ':data:api-builder'

Edit the data/api-builder/build.gradle file as follows:

data/api-builder/build.gradle

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.

app/build.gradle

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.

Partially extract code from AppModule to other classes

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.

ApiBuilder

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.

Adding a db module

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.

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.

Adding a testing-release module and a testing-debug module

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:

testing-release/build.gradle and testing-debug/build.gradle

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.

app/build.gradle

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

data/db/build.gradle

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
}

data/repository/build.gradle

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.

Moving db-related classes to a data/db module

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.

Adding a executor 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.

repository/build.gradle

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

app/build.gradle

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:

executor/build.gradle

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.

Moving repository-related classes to the data/repository 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.

data/repository/build.gradle

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.

Creating a feature directory

Create a feature directory, in the same way we created the data directory.

Adding a login module

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.

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:

feature/login/build.gradle

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.

app/build.gradle

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

Unable to reference app module's BuildConfig from the library module

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.

Adding an envvar module

Create the module and set its package name to com.android.example.envvar.

Edit the envvar/build.gradle file like so:

envvar/build.gradle

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.

feature/login/build.gradle

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

app/build.gradle

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.

EnvVar

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.

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.

LoginHelper

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.

Adding a di module

Create a di module and set its package nameはcom.android.example.di.

Edit the di/build.gradle file like so:

di/build.gradle

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.

app/build.gradle

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

feature/login/build.gradle

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.

Revert changes to /build.gradle made when adding modules

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:

build.gradle

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 unnecessary dependencies in app/build.gradle

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.