Skip to content

Commit da322a4

Browse files
authored
Android: add aar dependencies support, create proper build pipeline, add Jetpack Compose example (#4188)
This PR changes a lot of things in the Android build support (mainly focusing on creating proper build pipeline). In details: * `aar` dependencies processing * Manifest merging * Setting min/target/compile SDK properties in the build script properties instead of manifest * Proper work with `aapt2` at the `compile`/`link`stages * R classes generation for the libraries (hacky though) * Proguard definitions collection. NB: there is no support of obfuscation in this PR, Proguard here is supported only for the main DEX packing. * Clear separation between debug and release signing configs (release key shouldn't be a part of the required setup; user should be able to bootstrap project quickly and create release key only when it is really needed). * Changes to the Android build script APIs * Introduction of the debug vs release flags for `aapt2` and dexing * Plus it adds Jetpack Compose sample This PR **doesn't add yet** support of the transitive Android modules, this is a work to be done in the future. The code quality is maybe meh, but I expect many pieces here to be changed in the future to be better and have wider support of the different build options/flags. The most important part this PR does is mentioned above: it brings a proper pipeline. I added many flags/comments for the future work, allowing contributors to focus on the specific parts. One thing to mention as well is that in the current state the incremental builds will have terrible performance. This is the area which requires a massive time investment to be on-par with AGP to support only processing of the files in the pipeline which really changed. Because of this the output types of certain tasks will probably change in the future or they will be split into more granular tasks. I would like also to mention other things which should be improved: * Things like `def bundleToolVersion = "1.17.2"` should be pre-defined in the Android SDK module and they shouldn't be required for writing build script, because: * in this particular example bundle is not needed for APK modules or lib modules; * these things are really technical and it is not easy to get versions list without knowing artifact coordinates * ideally Mill should be able to fetch latest versions of such tooling by itself * Project structure in the Android examples created so far is very different from project structure created by Android Studio, this may bring additional complexity migrating existing projects. Maybe it is worth to create something similar to `MavenModule`? * Successful APK/bundle compilation doesn't guarantee that everything is alright, app may crash at runtime because something wasn't packed correctly. It is needed to add integration tests support on the Android emulator as a part of CI pipeline * **IMPORTANT**: Release key tasks for getting key alias/pass/store pass are leaking sensitive credentials, because outputs will be serialized to JSON. Sensitive data should never be exposed (let it be disk or console output). * Maybe it is worth to create `mill.androidlib`? * I think it is not worth to focus on Java samples right now, given that Kotlin is the preferred language for the Android development. * I don't thing creating many traits like `AndroidAppBundle` is a good idea, because it is just a way of packaging. AGP has only 2 main entrypoints: Application and Library, the rest is their configuration options. Bazel also has application and library as main entrypoints for rules (see https://github.com/bazelbuild/rules_android/tree/main/rules). * Default build tools version should be inferred from `compileSdk` version (e.g `compileSdk` is 35, then `buildToolsVersion` should be `35.0.0` by default), but in the current layout it is not possible to do implicitly, so both versions should be specified. Co-authored-by: 0xnm <[email protected]>
1 parent 08cbef1 commit da322a4

File tree

33 files changed

+1180
-288
lines changed

33 files changed

+1180
-288
lines changed

example/android/javalib/1-hello-world/build.mill

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,35 @@ import mill.javalib.android.AndroidTestModule
1818
// Create and configure an Android SDK module to manage Android SDK paths and tools.
1919
object androidSdkModule0 extends AndroidSdkModule {
2020
def buildToolsVersion = "35.0.0"
21-
def bundleToolVersion = "1.17.2"
2221
}
2322

2423
// Actual android application
2524
object app extends AndroidAppModule {
2625
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
26+
def androidMinSdk = 19
27+
def androidCompileSdk = 35
2728

2829
// Configuration for ReleaseKey
29-
override def releaseKeyPath = super.millSourcePath
30-
31-
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
32-
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
33-
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
34-
def androidReleaseKeyStorePass: T[String] = Task { "MillBuildTool" }
30+
def androidReleaseKeyName: T[Option[String]] = Task { Some("releaseKey.jks") }
31+
def androidReleaseKeyAlias: T[Option[String]] = Task { Some("releaseKey") }
32+
def androidReleaseKeyPass: T[Option[String]] = Task { Some("MillBuildTool") }
33+
def androidReleaseKeyStorePass: T[Option[String]] = Task { Some("MillBuildTool") }
3534

3635
override def androidVirtualDeviceIdentifier: String = "java-test"
3736

38-
private def mainRoot = millSourcePath
39-
4037
object test extends AndroidAppTests with TestModule.Junit4 {
4138
def testFramework = "com.novocode.junit.JUnitFramework"
4239
def ivyDeps = super.ivyDeps() ++ Agg(
4340
ivy"junit:junit:4.13.2"
4441
)
4542
}
4643

47-
object it extends AndroidAppIntegrationTests with AndroidTestModule.AndroidJUnit {
48-
def repositoriesTask = Task.Anon {
49-
super.repositoriesTask() ++
50-
Seq(MavenRepository("https://maven.google.com"))
51-
}
44+
object it extends AndroidAppInstrumentedTests with AndroidTestModule.AndroidJUnit {
5245

5346
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
5447

5548
override def instrumentationPackage = "com.helloworld.app"
5649

57-
/* TODO this needs to change to the location of the debug keystore once integration test */
58-
override def releaseKeyPath = mainRoot
59-
60-
def androidReleaseKeyName: T[String] = Task {
61-
"releaseKey.jks"
62-
}
63-
64-
def androidReleaseKeyAlias: T[String] = Task {
65-
"releaseKey"
66-
}
67-
68-
def androidReleaseKeyPass: T[String] = Task {
69-
"MillBuildTool"
70-
}
71-
72-
def androidReleaseKeyStorePass: T[String] = Task {
73-
"MillBuildTool"
74-
}
75-
7650
/* TODO currently the dependency resolution ignores the platform type and kotlinx-coroutines-core has
7751
* conflicting classes with kotlinx-coroutines-core-jvm . Remove the exclusions once the dependency
7852
* resolution resolves conflicts between androidJvm and jvm platform types

example/android/javalib/2-app-bundle/build.mill

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,16 @@ import mill.javalib.android.{AndroidSdkModule, AndroidAppBundle}
1212

1313
object androidSdkModule0 extends AndroidSdkModule {
1414
def buildToolsVersion = "35.0.0"
15-
def bundleToolVersion = "1.17.2"
1615
}
1716

1817
object bundle extends AndroidAppBundle {
1918
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
19+
def androidCompileSdk = 35
2020

21-
override def releaseKeyPath = millSourcePath
22-
23-
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
24-
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
25-
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
26-
def androidReleaseKeyStorePass: T[String] = Task { "MillBuildTool" }
21+
def androidReleaseKeyName: T[Option[String]] = Task { Some("releaseKey.jks") }
22+
def androidReleaseKeyAlias: T[Option[String]] = Task { Some("releaseKey") }
23+
def androidReleaseKeyPass: T[Option[String]] = Task { Some("MillBuildTool") }
24+
def androidReleaseKeyStorePass: T[Option[String]] = Task { Some("MillBuildTool") }
2725
}
2826

2927
////SNIPPET:END

example/android/javalib/3-linting/build.mill

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ import mill.javalib.android.{AndroidSdkModule, AndroidAppModule, AndroidLintRepo
1616
// Create and configure an Android SDK module to manage Android SDK paths and tools.
1717
object androidSdkModule0 extends AndroidSdkModule {
1818
def buildToolsVersion = "35.0.0"
19-
def bundleToolVersion = "1.17.2"
2019
}
2120

2221
// Actual android application with linting config
2322
object app extends AndroidAppModule {
2423
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
25-
override def releaseKeyPath = millSourcePath
24+
def androidCompileSdk = 35
2625

2726
// Set path to the custom `lint.xml` config file. It is usually at the root of the project
2827
def androidLintConfigPath = Task { Some(PathRef(millSourcePath / "lint.xml")) }

example/android/kotlinlib/1-hello-kotlin/build.mill

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,34 @@ import coursier.maven.MavenRepository
1919
// Create and configure an Android SDK module to manage Android SDK paths and tools.
2020
object androidSdkModule0 extends AndroidSdkModule {
2121
def buildToolsVersion = "35.0.0"
22-
def bundleToolVersion = "1.17.2"
2322
}
2423

2524
// Actual android application
2625
object app extends AndroidAppKotlinModule {
2726

2827
def kotlinVersion = "2.0.20"
2928
def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
29+
def androidMinSdk = 19
30+
def androidCompileSdk = 35
3031

3132
// Configuration for ReleaseKey
32-
override def releaseKeyPath = millSourcePath
33-
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
34-
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
35-
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
36-
def androidReleaseKeyStorePass: T[String] = Task { "MillBuildTool" }
33+
def androidReleaseKeyName: T[Option[String]] = Task { Some("releaseKey.jks") }
34+
def androidReleaseKeyAlias: T[Option[String]] = Task { Some("releaseKey") }
35+
def androidReleaseKeyPass: T[Option[String]] = Task { Some("MillBuildTool") }
36+
def androidReleaseKeyStorePass: T[Option[String]] = Task { Some("MillBuildTool") }
3737
override def androidVirtualDeviceIdentifier: String = "kotlin-test"
3838
override def androidEmulatorPort: String = "5556"
3939

40-
/* TODO this won't be needed once the debug keystore is implemented */
41-
private def mainRoot = millSourcePath
42-
4340
object test extends AndroidAppKotlinTests with TestModule.Junit4 {
4441
def ivyDeps = super.ivyDeps() ++ Agg(
4542
ivy"junit:junit:4.13.2"
4643
)
4744
}
4845

49-
object it extends AndroidAppKotlinIntegrationTests with AndroidTestModule.AndroidJUnit {
46+
object it extends AndroidAppKotlinInstrumentedTests with AndroidTestModule.AndroidJUnit {
5047

5148
override def instrumentationPackage = "com.helloworld.app"
5249

53-
/* TODO this needs to change to something better once integration tests work with debug keystore */
54-
override def releaseKeyPath = mainRoot
55-
def androidReleaseKeyName: T[String] = Task { "releaseKey.jks" }
56-
def androidReleaseKeyAlias: T[String] = Task { "releaseKey" }
57-
def androidReleaseKeyPass: T[String] = Task { "MillBuildTool" }
58-
def androidReleaseKeyStorePass: T[String] = Task { "MillBuildTool" }
59-
6050
/* TODO currently the dependency resolution ignores the platform type and kotlinx-coroutines-core has
6151
* conflicting classes with kotlinx-coroutines-core-jvm . Remove the exclusions once the dependency
6252
* resolution resolves conflicts between androidJvm and jvm platform types
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.example.composetest">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:dataExtractionRules="@xml/data_extraction_rules"
8+
android:fullBackupContent="@xml/backup_rules"
9+
android:icon="@mipmap/ic_launcher"
10+
android:label="@string/app_name"
11+
android:roundIcon="@mipmap/ic_launcher_round"
12+
android:supportsRtl="true"
13+
android:theme="@style/Theme.ComposeTest">
14+
<activity
15+
android:name=".MainActivity"
16+
android:exported="true"
17+
android:label="@string/app_name"
18+
android:theme="@style/Theme.ComposeTest">
19+
<intent-filter>
20+
<action android:name="android.intent.action.MAIN" />
21+
22+
<category android:name="android.intent.category.LAUNCHER" />
23+
</intent-filter>
24+
</activity>
25+
</application>
26+
27+
</manifest>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.example.composetest
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.material3.Button
12+
import androidx.compose.material3.ButtonDefaults
13+
import androidx.compose.material3.Scaffold
14+
import androidx.compose.material3.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.mutableIntStateOf
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.graphics.Color
21+
import androidx.compose.ui.unit.dp
22+
import com.example.composetest.ui.theme.ComposeTestTheme
23+
24+
class MainActivity : ComponentActivity() {
25+
override fun onCreate(savedInstanceState: Bundle?) {
26+
super.onCreate(savedInstanceState)
27+
enableEdgeToEdge()
28+
setContent {
29+
ComposeTestTheme {
30+
Scaffold(modifier = Modifier.fillMaxSize()) { _ ->
31+
CounterScreen(
32+
modifier = Modifier.padding(),
33+
)
34+
}
35+
}
36+
}
37+
}
38+
}
39+
40+
@Suppress("FunctionName")
41+
@Composable
42+
fun CounterScreen(modifier: Modifier = Modifier) {
43+
val clickCount = remember { mutableIntStateOf(0) }
44+
45+
Column(
46+
modifier = Modifier
47+
.fillMaxSize()
48+
.padding(16.dp),
49+
horizontalAlignment = Alignment.CenterHorizontally,
50+
verticalArrangement = Arrangement.Center,
51+
) {
52+
Text(
53+
text = "Button clicked: ${clickCount.intValue} times",
54+
modifier = Modifier.padding(bottom = 16.dp),
55+
)
56+
57+
Button(
58+
onClick = { clickCount.intValue++ },
59+
colors = ButtonDefaults.buttonColors().copy(
60+
containerColor = Color.Cyan,
61+
),
62+
) {
63+
Text("Click me!")
64+
}
65+
}
66+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.composetest.ui.theme
2+
3+
import androidx.compose.ui.graphics.Color
4+
5+
val Purple80 = Color(0xFFD0BCFF)
6+
val PurpleGrey80 = Color(0xFFCCC2DC)
7+
val Pink80 = Color(0xFFEFB8C8)
8+
9+
val Purple40 = Color(0xFF6650a4)
10+
val PurpleGrey40 = Color(0xFF625b71)
11+
val Pink40 = Color(0xFF7D5260)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.example.composetest.ui.theme
2+
3+
import android.os.Build
4+
import androidx.compose.foundation.isSystemInDarkTheme
5+
import androidx.compose.material3.MaterialTheme
6+
import androidx.compose.material3.darkColorScheme
7+
import androidx.compose.material3.dynamicDarkColorScheme
8+
import androidx.compose.material3.dynamicLightColorScheme
9+
import androidx.compose.material3.lightColorScheme
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.platform.LocalContext
12+
13+
private val DarkColorScheme = darkColorScheme(
14+
primary = Purple80,
15+
secondary = PurpleGrey80,
16+
tertiary = Pink80,
17+
)
18+
19+
private val LightColorScheme = lightColorScheme(
20+
primary = Purple40,
21+
secondary = PurpleGrey40,
22+
tertiary = Pink40,
23+
24+
/* Other default colors to override
25+
background = Color(0xFFFFFBFE),
26+
surface = Color(0xFFFFFBFE),
27+
onPrimary = Color.White,
28+
onSecondary = Color.White,
29+
onTertiary = Color.White,
30+
onBackground = Color(0xFF1C1B1F),
31+
onSurface = Color(0xFF1C1B1F), */
32+
)
33+
34+
@Suppress("FunctionName")
35+
@Composable
36+
fun ComposeTestTheme(
37+
darkTheme: Boolean = isSystemInDarkTheme(),
38+
// Dynamic color is available on Android 12+
39+
dynamicColor: Boolean = true,
40+
content: @Composable () -> Unit,
41+
) {
42+
val colorScheme = when {
43+
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44+
val context = LocalContext.current
45+
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46+
}
47+
48+
darkTheme -> DarkColorScheme
49+
else -> LightColorScheme
50+
}
51+
52+
MaterialTheme(
53+
colorScheme = colorScheme,
54+
typography = Typography,
55+
content = content,
56+
)
57+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.example.composetest.ui.theme
2+
3+
import androidx.compose.material3.Typography
4+
import androidx.compose.ui.text.TextStyle
5+
import androidx.compose.ui.text.font.FontFamily
6+
import androidx.compose.ui.text.font.FontWeight
7+
import androidx.compose.ui.unit.sp
8+
9+
// Set of Material typography styles to start with
10+
val Typography = Typography(
11+
bodyLarge = TextStyle(
12+
fontFamily = FontFamily.Default,
13+
fontWeight = FontWeight.Normal,
14+
fontSize = 16.sp,
15+
lineHeight = 24.sp,
16+
letterSpacing = 0.5.sp,
17+
),
18+
/* Other default text styles to override
19+
titleLarge = TextStyle(
20+
fontFamily = FontFamily.Default,
21+
fontWeight = FontWeight.Normal,
22+
fontSize = 22.sp,
23+
lineHeight = 28.sp,
24+
letterSpacing = 0.sp
25+
),
26+
labelSmall = TextStyle(
27+
fontFamily = FontFamily.Default,
28+
fontWeight = FontWeight.Medium,
29+
fontSize = 11.sp,
30+
lineHeight = 16.sp,
31+
letterSpacing = 0.5.sp
32+
) */
33+
)

0 commit comments

Comments
 (0)