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
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.