# Three-layer architecture following the Android architecture guide









<LinkCard
  title="App Architecture Guide"
  description="Android's official recommended architecture: the roles of the UI, domain, and data layers, and the direction of their dependencies."
  href="https://developer.android.com/topic/architecture"
/>

The app architecture guide published by Android Developers is widely known as the baseline architecture for Android applications.

Below is an example architecture definition for a setup that adopts a domain layer.

## Example directory structure

<FileTree>
- 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
</FileTree>

## Architecture definition

```kt
// ProjectArchitecture.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.architecture
import me.tbsten.katachi.dsl.gradle.gradle

/**
 * See https://developer.android.com/topic/architecture
 */
val projectArchitecture = architecture {
  gradle() // Gradle files such as the wrapper, settings and build scripts

  // 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.kt
const val appPackageDir = "com/example/project"

// roles/UiState.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.uiState() = "UiState" {
  summary = "Holds the state a screen renders, as an immutable value"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*UiState".ktFile()
  }

  shouldBeImmutable()
}

private fun RoleScope.shouldBeImmutable() =
  "All properties are val".konsist {
    classes().must { klass -> klass.properties().all { it.isVal } }
  }

// roles/ViewModel.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.viewModel() = "ViewModel" {
  summary = "Exposes a screen's state as a StateFlow and receives events from the screen"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*ViewModel".ktFile()
  }

  shouldContainsViewModelClass()
  shouldExtendsViewModel()
  shouldUiStateFlowBePublic()
}

private fun RoleScope.shouldContainsViewModelClass() =
  "Declares exactly one class named after the file".konsist {
    files.must { file -> file.classes().map { it.name } == listOf(file.name) }
  }

private fun RoleScope.shouldExtendsViewModel() =
  "Extends ViewModel".konsist {
    classes().must { klass -> klass.parents().any { it.name == "ViewModel" } }
  }

private fun RoleScope.shouldUiStateFlowBePublic() =
  "Exposes the UiState Flow as 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.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.screen() = "Screen" {
  summary = "The public Composable that draws a screen"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "ui/*" / "*Screen".ktFile()
  }

  shouldContainsPublicScreenComposable()
}

private fun RoleScope.shouldContainsPublicScreenComposable() =
  "Declares a public Composable named after the file".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.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.useCase() = "UseCase" {
  summary = "Runs one app-specific behavior through invoke"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "domain/*" / "*UseCase".ktFile()
  }

  shouldContainsUseCaseClass()
  shouldSingleInvokePublicMethod()
}

private fun RoleScope.shouldContainsUseCaseClass() =
  "Declares exactly one class named after the file".konsist {
    files.must { file -> file.classes().map { it.name } == listOf(file.name) }
  }

private fun RoleScope.shouldSingleInvokePublicMethod() =
  "Has exactly one public invoke method".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.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile

fun DeclarationContainerScope.domainModel() = "DomainModel" {
  summary = "Models that represent the app's concepts in the domain layer"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "domain/model" / "*".ktFile()
  }
}

// roles/RepositoryInterface.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.repositoryInterface() = "RepositoryInterface" {
  summary = "The interface that serves as the entry point for reading and storing data"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*Repository".ktFile()
  }

  shouldContainsRepositoryInterface()
}

private fun RoleScope.shouldContainsRepositoryInterface() =
  "Declares exactly one interface named after the file".konsist {
    files.must { file -> file.interfaces().map { it.name } == listOf(file.name) }
  }

// roles/RepositoryImplementation.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.repositoryImplementation() = "RepositoryImplementation" {
  summary = "The implementation of RepositoryInterface"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*RepositoryImpl".ktFile()
  }

  shouldContainsRepositoryImplementationClass()
}

private fun RoleScope.shouldContainsRepositoryImplementationClass() =
  "Declares exactly one class named after the file".konsist {
    files.must { file -> file.classes().map { it.name } == listOf(file.name) }
  }

// roles/DataSource.kt
@file:OptIn(ExperimentalKatachiApi::class)

import me.tbsten.katachi.ExperimentalKatachiApi
import me.tbsten.katachi.dsl.DeclarationContainerScope
import me.tbsten.katachi.dsl.RoleScope
import me.tbsten.katachi.dsl.gradle.kotlin
import me.tbsten.katachi.dsl.gradle.mainSourceSet
import me.tbsten.katachi.dsl.kotlin.ktFile
import me.tbsten.katachi.konsist.konsist

fun DeclarationContainerScope.dataSource() = "DataSource" {
  summary = "Access to the sources and stores of data that a Repository uses"
  layout {
    "app" / mainSourceSet / kotlin / appPackageDir / "data/*" / "*DataSource".ktFile()
  }

  shouldContainsDataSourceClass()
}

private fun RoleScope.shouldContainsDataSourceClass() =
  "Declares exactly one class named after the file".konsist {
    files.must { file -> file.classes().map { it.name } == listOf(file.name) }
  }
```

Because this uses `konsist { }`, add a dependency on `katachi-konsist` to `:architecture-test` and call `assert(FileConstraintCheck())` in the test. With `assert()` alone the constraints are not evaluated and the test fails with `[UncheckedFileConstraint]`. See [Konsist integration](/katachi/guides/konsist-integration/) for details.

A real definition that splits the layers into groups and the kinds of files into Roles is in [ProjectArchitecture.kt in sample/android](https://github.com/TBSten/katachi/blob/main/sample/android/architecture-test/src/test/kotlin/com/example/sample/ProjectArchitecture.kt). The sample has no domain layer and splits its modules by screen, so its definition is shaped differently from the one on this page.

<SnippetIsStartingPoint>
For example, if your product uses Dagger Hilt, you might add a check for the `@HiltViewModel` annotation on ViewModels, or a check that repository implementation classes have an `@Inject` constructor — customizations like these will likely be necessary.
</SnippetIsStartingPoint>