Android architecture guide に従った3層アーキテクチャ
アプリ アーキテクチャ ガイドAndroid 公式の推奨アーキテクチャ。UI 層・ドメイン層・データ層の役割と依存の向き。

Android developers で公開されているアプリアーキテクチャガイドは、Android アプリケーションのベースとなるアーキテクチャとして広く知られています。
以下は Domain レイヤーを採用する場合のアーキテクチャ定義の例です。
ディレクトリ構造の例
Section titled “ディレクトリ構造の例”ディレクトリapp/src/main/kotlin/com/example/project/
ディレクトリui/
ディレクトリhome/
- HomeUiState.kt
- HomeViewModel.kt
- HomeScreen.kt
ディレクトリdomain/
ディレクトリuser/
- GetUserUseCase.kt
ディレクトリmodel/
- User.kt
ディレクトリdata/
ディレクトリuser/
- UserRepository.kt
- UserRepositoryImpl.kt
- UserRemoteDataSource.kt
アーキテクチャ定義
Section titled “アーキテクチャ定義”@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.architectureimport me.tbsten.katachi.dsl.gradle.gradle
/** * See https://developer.android.com/topic/architecture */val projectArchitecture = architecture { gradle() // wrapper・settings・build スクリプトなど Gradle のファイル
// https://developer.android.com/topic/architecture/ui-layer "ui".group { uiState() viewModel() screen() }
// https://developer.android.com/topic/architecture/domain-layer "domain".group { useCase() domainModel() }
// https://developer.android.com/topic/architecture/data-layer "data".group { repositoryInterface() repositoryImplementation() dataSource() }}
// Constants.ktconst val appPackageDir = "com/example/project"
// roles/UiState.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.uiState() = "UiState" { summary = "画面が描画に使う状態を、変更されない値として持つ" layout { "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*UiState".ktFile() }
shouldBeImmutable()}
private fun RoleScope.shouldBeImmutable() = "プロパティはすべて val であること".konsist { classes().must { klass -> klass.properties().all { it.isVal } } }
// roles/ViewModel.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.viewModel() = "ViewModel" { summary = "画面の状態を StateFlow で公開し、画面からのイベントを受け取る" layout { "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*ViewModel".ktFile() }
shouldContainsViewModelClass() shouldExtendsViewModel() shouldUiStateFlowBePublic()}
private fun RoleScope.shouldContainsViewModelClass() = "ファイル名と同じ名前のクラスを1つだけ宣言すること".konsist { files.must { file -> file.classes().map { it.name } == listOf(file.name) } }
private fun RoleScope.shouldExtendsViewModel() = "ViewModel を継承すること".konsist { classes().must { klass -> klass.parents().any { it.name == "ViewModel" } } }
private fun RoleScope.shouldUiStateFlowBePublic() = "UiState の Flow を public で公開すること".konsist { classes().must { klass -> klass.properties().any { it.hasPublicOrDefaultModifier && it.type?.name?.startsWith("StateFlow") == true } } }
// roles/Screen.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.screen() = "Screen" { summary = "画面を描く public な Composable" layout { "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*Screen".ktFile() }
shouldContainsPublicScreenComposable()}
private fun RoleScope.shouldContainsPublicScreenComposable() = "ファイル名と同じ名前の public な Composable を宣言すること".konsist { files.must { file -> file.functions().any { it.name == file.name && it.hasPublicOrDefaultModifier && it.hasAnnotationWithName("Composable") } } }
// roles/UseCase.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.useCase() = "UseCase" { summary = "アプリ固有の1つの振る舞いを invoke で実行する" layout { "app" / mainSourceSet / kotlin / appPackageDir / "domain/*" / "*UseCase".ktFile() }
shouldContainsUseCaseClass() shouldSingleInvokePublicMethod()}
private fun RoleScope.shouldContainsUseCaseClass() = "ファイル名と同じ名前のクラスを1つだけ宣言すること".konsist { files.must { file -> file.classes().map { it.name } == listOf(file.name) } }
private fun RoleScope.shouldSingleInvokePublicMethod() = "public な invoke メソッドを1つだけ持つこと".konsist { classes().must { klass -> klass.functions().filter { it.hasPublicOrDefaultModifier }.map { it.name } == listOf("invoke") } }
// roles/DomainModel.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFile
fun DeclarationContainerScope.domainModel() = "DomainModel" { summary = "ドメイン層で扱う、アプリの概念を表すモデル" layout { "app" / mainSourceSet / kotlin / appPackageDir / "domain/model" / "*".ktFile() }}
// roles/RepositoryInterface.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.repositoryInterface() = "RepositoryInterface" { summary = "データの取得と保存の窓口となる interface" layout { "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*Repository".ktFile() }
shouldContainsRepositoryInterface()}
private fun RoleScope.shouldContainsRepositoryInterface() = "ファイル名と同じ名前の interface を1つだけ宣言すること".konsist { files.must { file -> file.interfaces().map { it.name } == listOf(file.name) } }
// roles/RepositoryImplementation.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.repositoryImplementation() = "RepositoryImplementation" { summary = "RepositoryInterface の実装" layout { "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*RepositoryImpl".ktFile() }
shouldContainsRepositoryImplementationClass()}
private fun RoleScope.shouldContainsRepositoryImplementationClass() = "ファイル名と同じ名前のクラスを1つだけ宣言すること".konsist { files.must { file -> file.classes().map { it.name } == listOf(file.name) } }
// roles/DataSource.kt@file:OptIn(ExperimentalKatachiApi::class)
import me.tbsten.katachi.ExperimentalKatachiApiimport me.tbsten.katachi.dsl.DeclarationContainerScopeimport me.tbsten.katachi.dsl.RoleScopeimport me.tbsten.katachi.dsl.gradle.kotlinimport me.tbsten.katachi.dsl.gradle.mainSourceSetimport me.tbsten.katachi.dsl.kotlin.ktFileimport me.tbsten.katachi.konsist.konsist
fun DeclarationContainerScope.dataSource() = "DataSource" { summary = "Repository が使う、データの取得元・保存先へのアクセス" layout { "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*DataSource".ktFile() }
shouldContainsDataSourceClass()}
private fun RoleScope.shouldContainsDataSourceClass() = "ファイル名と同じ名前のクラスを1つだけ宣言すること".konsist { files.must { file -> file.classes().map { it.name } == listOf(file.name) } }konsist { } を使うので、:architecture-test に katachi-konsist への依存を足し、テストでは assert(FileConstraintCheck()) を呼んでください。assert() だけでは制約が評価されず、[UncheckedFileConstraint] で落ちます。詳しくは Konsist との統合 を参照してください。
層を group に、ファイルの種類を Role に分けた実際の定義は、sample/android の ProjectArchitecture.kt にあります。サンプルはドメイン層を持たず、画面ごとにモジュールを分けた構成なので、このページの定義とは形が違います。