このコードラボでは、既存のAndroidアプリをmulti-module project化する方法を学びます。
このようにapp moduleだけで構成されているprojectを、
下記の図の通り、multi-module project化します。
以下のリンクからこのコードラボで必要なコードすべてをダウンロードすることができます。
または、以下のコマンドでコマンドラインからGitHubリポジトリをcloneすることもできます。
$ git clone https://github.com/DroidKaigi/architecture-components-samples.git
今回のコードラボで使用するブランチは下記です。
まずはそのままの状態でサンプルアプリがどういう構造になっているかをみてみましょう。次の手順に従ってAndroid Studioでサンプルアプリを開きます。
architecture-components-samples-droidkaigi-2020-codelab.zip
ファイルをダウンロードしている場合は、ファイルを展開しますGithubBrowserSample
プロジェクトを開きますGitHubのログイン画面が表示された場合、ログインをお願いします。
(ログインしないと、GitHub APIのRate Limitにすぐ引っかかってしまうためです)
下記のようなgithub.comを開くアプリを選択する画面が表示された場合、Chromeを選択してGitHubにログインしてください。
このサンプルアプリは、GitHubのリポジトリを検索することができます。
試しに、「droidkaigi」で検索してみましょう。
無事にリポジトリの検索結果が表示されるはずです。
それでは、まずapiを別moduleに分割してみましょう。
ディレクトリ構造はDroidKaigi/conference-app-2020を参考に、dataディレクトリにapiやrepositoryといったmoduleを配置していきます。
まず、dataディレクトリを作成します。
作業がしやすいようにProjectのViewをProject Filesに切り替えておきます。
package nameはcom.android.example.data.api
で作成します。
settings.gradleにも反映しておきましょう。その後、ビルドが通るか確認しておきましょう。
include ':app',
':data:api'
AccessTokenParameterをドラッグ・アンド・ドロップで、data/api/src/main/java/com.android.example.data.apiに移動してみます。
下記の画面が表示されるので、Refactorを押します。
しかし、下記の問題が検出され、本当に移動するのか確認されます。
ここでは内容を見てからCancelを押しましょう。
何が問題なのでしょうか?
com.google.gson.annotations.SerializedNameを参照しているが、これをdata/api moduleは知らないのが問題です。
data/apiのbuild.gradleを開き、依存を追加していきます。
ここでは、app moduleのcom.android.example.github.apiパッケージ配下のクラスが依存しているライブラリを追加しましょう。
app/build.gradleを参考に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
}
app/build.gradleに、data/api moduleへの依存を追加します。
add
というコメントがある行が、追加した行です。Gradle Syncをお忘れ無く!
dependencies {
implementation project(":data:api") // add
implementation deps.app_compat
implementation deps.recyclerview
念のため、ビルドが通ることを確認しておきましょう。
試して見て下さい!今度は無事に成功します。
同様にApiResponseとAccessTokenResponse、GithuthAuthServiceも移動しておきましょう。
移動後は、ビルドが通ることを確認しておきましょう。module分割作業中は、こまめにビルドが通るか確認することをお勧めします。
勘の良い方はお気づきかもしれませんが、残りのapiパッケージ配下のクラスはこのままではdata/api moduleに移動できません。
app module 内の別のクラスに依存しているからです。
それでは、次のセクションで残りのクラスをmodule分割できるようにしていきましょう。
GithubServiceとRepoSearchResponseはcom.android.example.github.vo配下のクラスに依存しています。
ここでは各module間でデータの受け渡しに使用する共通クラスの置き場所としてmodel moduleを導入。GithubServiceとRepoSearchResponseのapp moduleへの依存を切り離し、data/api moduleへの移動を可能とします。
初めてのmodule分割の手順を参考に、projectのrootにmodel moduleを追加します。
package nameはcom.android.example.model
で作成します。
com.android.example.github.vo配下のクラスが依存しているライブラリを、mode/build.gradleに記述します。
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
}
app moduleとapi moduleからmodel moduleへの依存を記述します。
dependencies {
implementation project(":model") // add
implementation project(":data:api")
dependencies {
api project(":model") // add
implementation deps.lifecycle.livedata_ktx
RepoSearchResult以外のクラスは、model moduleに移動可能です。1クラスずつ移動しましょう。
RepoSearchResultはapp module内のGithubTypeConvertersに依存しているため、model moduleに移動はできません。
各クラスの依存関係に注意しながら順番にクラスを移動しましょう。
これで、app moduleに残っているapi関連クラスを、AuthenticationInterceptor以外はapi moduleへ移動が可能になりました。
試しにGithubServiceのimport文をみてみましょう。
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
LiveDataやretrofit2など公開されているライブラリ以外には、library moduleのimport文しか存在しません。
それでは、RepoSearchResponseとGithubServiceをdata/api moduleに移動しましょう。
この状態でビルドすると、Data binding compilerで下記のエラーが発生します。
Android Studioのリファクタリング機能のバグなのか、
layoutファイル内のvariableのtypeが適切に書き換えられていません。
エラーが出ているレイアウトファイルを修正しましょう。
<variable
name="searchResult"
type="com.android.example.model.Resource" />
を
<variable
name="searchResult"
type="LiveData<Resource<List<Repo>>>" />
に変更します。
<variable
name="repo"
type="com.android.example.model.Resource" />
を
<variable
name="repo"
type="LiveData<Resource<Repo>>>" />
に変更します。
<variable
name="user"
type="com.android.example.model.Resource" />
を
<variable
name="user"
type="LiveData<Resource<User>>" />
に変更します。
これで、api関連クラスでdata/api moduleに移動できていないのはAuthenticationInterceptorだけになりました。
AuthenticationInterceptorはAccessTokenRepositoryに依存しているので、今度はreposiotry moduleを作りましょう。
初めてのmodule分割の手順を参考に、repository moduleを作ります。
package nameはcom.android.example.data.repository
で作成します。
repisotry moduleはdataディレクトリに移動し、settings.gradle内の名称も変更しておきましょう。
include ':app',
':model',
':data:repository',
':data:api'
全てのreposiotry関連クラスを一度にdata/repository moduleへ移動するのは大変なので、まずは依存しているものが少ないAccessTokenRepositoryだけをdata/repository moduleへ移動します。
まず、data/repository/build.gradleを変更します。そして、app moduleからdata/repository 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
}
app moduleからdata/repository moduleへの依存を記述します。
dependencies {
implementation project(":model")
implementation project(":data:api")
implementation project(":data:repository") // add
implementation deps.app_compat
implementation deps.recyclerview
これで、AccessTokenRepositoryをdata/repository moduleへ移動できます。
では、AuthenticationInterceptorをdata/api moduleに移動し、api moduleからdata/repository moduleに依存させれば良いでしょうか。いえ、そう簡単にはいきません。
repository moduleからapi moduleを参照し、repositoryでapiアクセスを隠蔽したいところですが、module間の依存で循環参照はおこなえません。
この場合、いくつかの対策が考えられますが、今回はGithubServiceとGithubAuthServiceをbuildするapi-builder moduleを追加し、ここにAuthenticationInterceptorを移動します。
そうすると、各moduleの依存関係が循環するのを防ぐことができます。
実際にやってみましょう。
初めてのmodule分割の手順を参考に、api-builder moduleを作ります。
package nameはcom.android.example.data.api_builder
で作成します。
api-builder moduleはdataディレクトリに移動し、settings.gradle内の名称も変更しておきましょう。
include ':app',
':model',
':data:repository',
':data:api',
':data:api-builder'
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
}
app moduleから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
AuthenticationInterceptorをdata/api-builder moduleに移動します。
AppModule#provideGithubService と AppModule#provideGithubAuthService の処理を他のクラスに切り出し、これをdata/api-builder moduleに移動しましょう。ここでは、 ApiBuilderというクラスを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)
}
}
AppModule で 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/"
)
}
これで
をapi-builder moduleへ移動できます。移動してください。
次は、残りのrepository関連クラスをdata/repository moduleに移動するため、それらが依存しているdb関係のクラス用のmoduleとしてdata/db moduleを導入し、移動していきましょう。
初めてのmodule分割の手順を参考に、db moduleを作ります。
package nameはcom.android.example.data.db
で作成します。
db moduleはdataディレクトリに移動し、settings.gradle内の名称も変更しておきましょう。
include ':app',
':model',
':data:db',
':data:repository',
':data:api',
':data:api-builder'
次に、db関係クラスと各repositoryクラスが依存しているOpenForTestingアノテーションを、各moduleから参照できるようにします。
OpenForTesting用のmoduleを作成、各々のmoduleからの依存関係を記述します。
ただし、OpenForTestingはreleaseとdebugで実装が異なるので、moduleも二つ作成します。
初めてのmodule分割の手順を参考に、testing-release moduleとtesting-debug moduleを作ります。
どちらもpackage nameはcom.android.example.testing
で作成します。
それぞれの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
}
各moduleのbuild.gradleにtesting-release moduleとtesting-debug moduleへの依存を追加します。
app moduleからdata/db moduleへの依存も追加します。
また、data/repository moduleからdata/api 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
次にBuild Variantsをreleaseにして、app/src/release に格納されているOpenForTesting.ktをtesting-release moduleへ移動します。
Android Studioのリファクタリング機能は、現在有効なBuild Variantsでしか機能しないため、注意して下さい。
次に、Build Variantsをdebugに戻して、app/src/debug に格納されているOpenForTesting.ktをtesting-debug moduleへ移動します。
念のため、releaseとdebugの両方のBuild Variantsでビルドが成功するか確認しておきましょう。
RepoSearchResultと、com.android.example.github.db配下のクラスを、全てdb moduleに移動します。
さて、他にrepositoryクラス達が依存しているクラスには何があるでしょうか?
あとはAppExecutorsを別moduleに切り出せば、repositoryクラス達をdata/repository moduleに移動できそうです。
AppExecutorsはexecutor moduleを作り、そちらに移動しましょう。
初めてのmodule分割の手順を参考に、executor moduleを作ります。
package nameはcom.android.example.executor
で作成します。
repository moduleとapp moduleを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
executor moduleにAppExecutorsを移動する前準備として、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
}
AppExecutorsをexecutor moduleに移動します。
各repositoryクラスが依存しているAbsentLiveDataとRateLimiterを先にdata/repository moduleに移動してください。
そして、data/repository moduleを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
その後にcom.android.example.github.repository配下のクラスを全てrepository moduleに移動します。
これで、app moduleからそれぞれ下記のlibrary moduleに分割することができました。
これまでのmodule分割では画面を含まないクラスばかりを対象としていました。
今回はログイン画面をmodule分割してみましょう。
dataディレクトリと同じように、featureディレクトリを作成しましょう。
package nameはcom.android.example.feature.login
で作成します。
login moduleはfeatureディレクトリに移動し、settings.gradle内の名称も変更しておきましょう。
include ':app',
':feature:login',
':executor',
':testing-release',
':testing-debug',
':model',
':data:db',
':data:repository',
':data:api',
':data:api-builder'
feature/login moduleにはLoginActivityとLoginHelperを移動します。
あらかじめ、これを考慮してfeature/login moduleの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
}
app moduleから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(":feature:login") // add
implementation deps.app_compat
implementation deps.recyclerview
LoginHelperでは下記のBuildConfigを参照しています。
しかし、これはあくまでapp module側で定義しているBuildConfigなので、LoginHelperをfeature/login moduleに移動すると参照できません。
今回はBuildConfigをラップして提供するためのEnvVarというinterfaceを定義し、これをenvvar moduleに配置。EnvVarの実装自体はapp moduleでおこなう、という方法で対処します。
package nameはcom.android.example.envvar
で作成します。
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
}
feature/login moduleとapp moduleをenvvar 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
次に、envvar moduleにEnvVar interfaceを追加します。
package com.android.example.envvar
interface EnvVar {
val GITHUB_CLIENT_ID: String
val GITHUB_CLIENT_SECRET: String
}
AppModuleでEnvVarを実装し、提供します。
@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
}
}
LoginHelperはBuildConfigを直接参照するのを辞め、代わりにEnvVarを使うように変更します。
ここでのBuildConfigはcom.android.example.github.BuildConfigを参照できるようにimportしておきましょう。
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
}
}
これで、LoginHelperはfeature/login moduleへ移動可能です。移動しましょう。
さて、LoginActivityはInjectableというinterfaceに依存しており、このままではLoginActivityをapp moduleの外に移動することはできません。
新たにdi moduleを作成し、これにはInjectableを移動しましょう。
package nameはcom.android.example.di
で作成します。
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
}
app moduleとfeature/login moduleにdi 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
次にInjectableをdi moduleに移動します。
これで、ようやくLoginActivityをfeaure/login moduleへ移動可能となりました。
activity_login.xml、LoginActvityの順にfeaure/login moduleへ移動しましょう。
Android Studio で module を追加すると、プロジェクトルートの build.gradle に不要なkotlin-gradle-plugin への依存が追加されます。削除しておきましょう。
最終的な 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
}
app/build.gradleの不要な依存性も削除しましょう。いくつかの依存は各library moduleのbuild.gradleに記述があれば問題ありません。
最終的な app/build.gradle の dependencies は下記になります。
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
}
いかがでしたでしょうか?
今回は開発開始してからほぼ3年が経過しているアプリを用い、実践的なmodule分割をおこないました。
codelabsという特性上、細かくmodule分割をおこなっています。
実際はexecutorやenvvar、diといった小さな単位でmoduleをつくるのではなく、例えばcore moduleを作り、そちらに集約する、という手もあります。
ただし、あまりに汎用的なmodule名(例:common)にしてしまうと、色々なクラスが存在する神moduleになってしまう危険性もあります。名前に気をつけたり、moduleが大きくなってきたら分割するなどをお勧めします。
また、今回はcodelabsのボリューム上、multi moduleにおいてDagger周りをどうするか?という部分には触れていません。
ぜひ、DroidKaigi Conference appなどを参考に、ご自身でDagger周りの改善をおこなってみてください。