Android Jetpack Compose

This page provides an example of using Mill as a build tool against more real like examples, using the official compose samples

Example Apps

Android Mill Setup for building JetLagged and JetNews

build.mill (download, browse)
import mill.*, androidlib.*, kotlinlib.*

object Versions {
  val kotlinVersion = "2.1.20"
  val kotlinLanguageVersion = "1.9"

  val androidCompileSdk = 33
  val androidMinSdk = 21
}

Create and configure an Android SDK module to manage Android SDK paths and tools.

object androidSdkModule0 extends AndroidSdkModule {
  def buildToolsVersion = "35.0.0"
}

object JetLagged extends mill.api.Module {
  object app extends AndroidAppKotlinModule, AndroidR8AppModule {
    def kotlinVersion = Versions.kotlinVersion

    def kotlinLanguageVersion = Versions.kotlinLanguageVersion

    def androidIsDebug = true

    // FIXME: ideally R8 should compile without erroring, but the app seems to be working
    // without some reportedly missing classes.
    override def androidR8Args = Seq("--map-diagnostics", "error", "warning")

    override def androidDebugSettings: T[AndroidBuildTypeSettings] = Task {
      AndroidBuildTypeSettings(
        isMinifyEnabled = false,
        isShrinkEnabled = false
      ).withDefaultProguardFile("proguard-android-optimize.txt")
        .withProguardLocalFiles(
          Seq(
            moduleDir / "proguard-rules.pro"
          )
        )
    }

    override def androidApplicationNamespace = "com.example.jetlagged"

    override def androidApplicationId = "com.example.jetlagged"

    override def kotlincOptions = super.kotlincOptions() ++ Seq(
      "-jvm-target",
      "17"
    )

    def bomMvnDeps = super.mvnDeps() ++ Seq(
      mvn"androidx.compose:compose-bom:2025.05.00"
    )

    def mvnDeps = super.mvnDeps() ++ Seq(
      mvn"com.google.accompanist:accompanist-adaptive:0.37.3",
      mvn"androidx.appcompat:appcompat:1.7.0",
      mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2",
      mvn"androidx.concurrent:concurrent-futures:1.1.0",
      mvn"androidx.core:core-ktx:1.16.0",
      mvn"androidx.activity:activity-compose:1.10.1",
      mvn"androidx.lifecycle:lifecycle-common:2.9.0",
      mvn"androidx.lifecycle:lifecycle-process:2.9.0",
      mvn"androidx.lifecycle:lifecycle-runtime-compose:2.9.0",
      mvn"androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0",
      mvn"androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0",
      mvn"androidx.navigation:navigation-compose:2.9.0",
      mvn"androidx.emoji2:emoji2:1.5.0",
      mvn"androidx.emoji2:emoji2-views:1.5.0",
      mvn"androidx.emoji2:emoji2-bundled:1.5.0",
      mvn"androidx.window:window:1.4.0",
      mvn"androidx.window.extensions.core:core:1.0.0",
      mvn"androidx.constraintlayout:constraintlayout-compose:1.1.1",
      mvn"io.coil-kt:coil-compose:2.7.0",
      mvn"androidx.customview:customview-poolingcontainer:1.0.0",
      mvn"androidx.tracing:tracing:1.2.0",
      mvn"org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1",

      // version is resolved from compose-bom
      mvn"androidx.compose.runtime:runtime",
      mvn"androidx.compose.foundation:foundation",
      mvn"androidx.compose.foundation:foundation-layout",
      mvn"androidx.compose.ui:ui-util",
      mvn"androidx.compose.material3:material3",
      mvn"androidx.compose.animation:animation",
      mvn"androidx.compose.animation:animation-tooling-internal",
      mvn"androidx.compose.material:material-icons-extended",
      mvn"androidx.compose.material:material",
      mvn"androidx.compose.material3:material3-window-size-class",
      mvn"androidx.compose.ui:ui-text-google-fonts",
      mvn"androidx.compose.ui:ui-tooling-preview",
      mvn"androidx.compose.ui:ui-unit",
      mvn"androidx.compose.ui:ui-text",
      mvn"androidx.compose.ui:ui-graphics",

      // debug dependencies
      mvn"androidx.compose.ui:ui-tooling",
      mvn"androidx.compose.ui:ui-test-manifest"
    )

    def androidEnableCompose = true
    override def kotlinUseEmbeddableCompiler: Task[Boolean] = Task { true }

    def androidSdkModule = mill.api.ModuleRef(androidSdkModule0)

    def androidCompileSdk = Versions.androidCompileSdk

    def androidMinSdk = Versions.androidMinSdk

    object androidTest extends AndroidAppKotlinInstrumentedTests, AndroidR8AppModule,
          AndroidTestModule.AndroidJUnit {

      def bomMvnDeps = super.mvnDeps() ++ Seq(
        mvn"androidx.compose:compose-bom:2025.05.00"
      )

      // TODO consider defaulting this to the parent app value
      override def androidEnableCompose = true

      // TODO consider defaulting this to the parent app value
      override def kotlinUseEmbeddableCompiler: Task[Boolean] = Task {
        true
      }

      // FIXME: ideally R8 should compile without erroring, but the app seems to be working
      // without some reportedly missing classes.
      override def androidR8Args = Seq("--map-diagnostics", "error", "warning")

      def mvnDeps = super.mvnDeps() ++ Seq(
        mvn"junit:junit:4.13.2",
        mvn"androidx.test:core:1.6.1",
        mvn"androidx.test:runner:1.6.1",
        mvn"androidx.test.espresso:espresso-core:3.6.1",
        mvn"androidx.test:rules:1.6.1",
        mvn"androidx.test.ext:junit:1.2.1",
        mvn"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2",
        mvn"androidx.compose.ui:ui-test",
        mvn"androidx.compose.ui:ui-test-junit4"
      )
    }
  }
}

object JetNews extends mill.api.Module {
  object app extends AndroidAppKotlinModule, AndroidR8AppModule {
    def kotlinVersion = Versions.kotlinVersion

    def kotlinLanguageVersion = Versions.kotlinLanguageVersion

    def androidIsDebug = true

    // FIXME: ideally R8 should compile without erroring, but the app seems to be working
    // without some reportedly missing classes.
    override def androidR8Args = Seq("--map-diagnostics", "error", "warning")

    override def androidDebugSettings: T[AndroidBuildTypeSettings] = Task {
      AndroidBuildTypeSettings(
        isMinifyEnabled = false,
        isShrinkEnabled = false
      ).withDefaultProguardFile("proguard-android-optimize.txt")
        .withProguardLocalFiles(
          Seq(
            moduleDir / "proguard-rules.pro"
          )
        )
    }

    override def androidApplicationNamespace = "com.example.jetnews"

    override def androidApplicationId = "com.example.jetnews"

    override def kotlincOptions = super.kotlincOptions() ++ Seq(
      "-jvm-target",
      "17"
    )

    def bomMvnDeps = super.mvnDeps() ++ Seq(
      mvn"androidx.compose:compose-bom:2025.05.00"
    )

    def mvnDeps = super.mvnDeps() ++ Seq(
      mvn"com.google.accompanist:accompanist-adaptive:0.37.3",
      mvn"androidx.appcompat:appcompat:1.7.0",
      mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2",
      mvn"androidx.concurrent:concurrent-futures:1.1.0",
      mvn"androidx.core:core-ktx:1.16.0",
      mvn"androidx.activity:activity-compose:1.10.1",
      mvn"androidx.lifecycle:lifecycle-common:2.9.0",
      mvn"androidx.lifecycle:lifecycle-process:2.9.0",
      mvn"androidx.lifecycle:lifecycle-runtime-compose:2.9.0",
      mvn"androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0",
      mvn"androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0",
      mvn"androidx.navigation:navigation-compose:2.9.0",
      mvn"androidx.emoji2:emoji2:1.5.0",
      mvn"androidx.emoji2:emoji2-views:1.5.0",
      mvn"androidx.emoji2:emoji2-bundled:1.5.0",
      mvn"androidx.window:window:1.4.0",
      mvn"androidx.window.extensions.core:core:1.0.0",
      mvn"androidx.constraintlayout:constraintlayout-compose:1.1.1",
      mvn"io.coil-kt:coil-compose:2.7.0",
      mvn"androidx.customview:customview-poolingcontainer:1.0.0",
      mvn"androidx.tracing:tracing:1.2.0",
      mvn"org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1",
      mvn"androidx.glance:glance-appwidget:1.2.0-alpha01",
      mvn"androidx.glance:glance-material3:1.2.0-alpha01",

      // version is resolved from compose-bom
      mvn"androidx.compose.runtime:runtime",
      mvn"androidx.compose.foundation:foundation",
      mvn"androidx.compose.foundation:foundation-layout",
      mvn"androidx.compose.ui:ui-util",
      mvn"androidx.compose.material3:material3",
      mvn"androidx.compose.animation:animation",
      mvn"androidx.compose.animation:animation-tooling-internal",
      mvn"androidx.compose.material:material-icons-extended",
      mvn"androidx.compose.material:material",
      mvn"androidx.compose.material3:material3-window-size-class",
      mvn"androidx.compose.ui:ui-text-google-fonts",
      mvn"androidx.compose.ui:ui-tooling-preview",
      mvn"androidx.compose.ui:ui-unit",
      mvn"androidx.compose.ui:ui-text",
      mvn"androidx.compose.ui:ui-graphics",

      // debug dependencies
      mvn"androidx.compose.ui:ui-tooling",
      mvn"androidx.compose.ui:ui-test-manifest"
    )

    def androidEnableCompose = true
    override def kotlinUseEmbeddableCompiler: Task[Boolean] = Task { true }

    def androidSdkModule = mill.api.ModuleRef(androidSdkModule0)

    def androidCompileSdk = Versions.androidCompileSdk

    def androidMinSdk = Versions.androidMinSdk

    object androidTest extends AndroidAppKotlinInstrumentedTests, AndroidR8AppModule,
          AndroidTestModule.AndroidJUnit {

      def bomMvnDeps = super.mvnDeps() ++ Seq(
        mvn"androidx.compose:compose-bom:2025.05.00"
      )

      // TODO consider defaulting this to the parent app value
      override def androidEnableCompose = true

      // TODO consider defaulting this to the parent app value
      override def kotlinUseEmbeddableCompiler: Task[Boolean] = Task {
        true
      }

      // FIXME: ideally R8 should compile without erroring, but the app seems to be working
      // without some reportedly missing classes.
      override def androidR8Args = Seq("--map-diagnostics", "error", "warning")

      def mvnDeps = super.mvnDeps() ++ Seq(
        mvn"junit:junit:4.13.2",
        mvn"androidx.test:core:1.6.1",
        mvn"androidx.test:runner:1.6.1",
        mvn"androidx.test.espresso:espresso-core:3.6.1",
        mvn"androidx.test:rules:1.6.1",
        mvn"androidx.test.ext:junit:1.2.1",
        mvn"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2",
        mvn"androidx.compose.ui:ui-test",
        mvn"androidx.compose.ui:ui-test-junit4"
      )
    }
  }
}

The compose samples repository, contains many sample apps! The following example is for building, testing and running JetNews.

> ./mill JetNews.app.androidApk

> ./mill show JetLagged.app.createAndroidVirtualDevice
...Name: test, DeviceId: medium_phone...

> ./mill show JetLagged.app.startAndroidEmulator

> ./mill show JetNews.app.androidInstall
...All files should be loaded. Notifying the device...

> ./mill show JetNews.app.androidRun --activity com.example.jetnews.ui.MainActivity
[
  "Starting: Intent { cmp=com.example.jetnews/.ui.MainActivity }",
  "Status: ok",
  "LaunchState: COLD",
  "Activity: com.example.jetnews/.ui.MainActivity",
  "TotalTime: ...",
  "WaitTime: ...",
  "Complete"
]

> ./mill show JetNews.app.androidTest
{
  "msg": "",
  "results": [
    {
      "fullyQualifiedName": "com.example.jetnews.HomeScreenTests.postsContainError_snackbarShown",
      "selector": "com.example.jetnews.HomeScreenTests.postsContainError_snackbarShown",
      "duration": ...,
      "status": "Success"
    },
    {
      "fullyQualifiedName": "com.example.jetnews.JetnewsTests.app_opensArticle",
      "selector": "com.example.jetnews.JetnewsTests.app_opensArticle",
      "duration": ...,
      "status": "Success"
    },
    {
      "fullyQualifiedName": "com.example.jetnews.JetnewsTests.app_launches",
      "selector": "com.example.jetnews.JetnewsTests.app_launches",
      "duration": ...,
      "status": "Success"
    },
    {
      "fullyQualifiedName": "com.example.jetnews.JetnewsTests.app_opensInterests",
      "selector": "com.example.jetnews.JetnewsTests.app_opensInterests",
      "duration": ...,
      "status": "Success"
    }
  ]
}

> ./mill show JetNews.app.stopAndroidEmulator

> ./mill show JetNews.app.deleteAndroidVirtualDevice

This example demonstrates how to build multiple Android Apps from the same project, translating their Gradle configuration to Mill and using R8 to optimise the App in a similar configuration as the original Gradle setup.