Android Starter: No Assembly Required
A modern Android template for small projects - that's how big projects start, isn't it?
Small projects and prototypes should start fast and arrive with good coding practices already wired in.
As a developer you don’t often start new projects, most of the time it’s when you want to try and learn something new. Usually it’s a small project and you want to start fast and get to coding right away. At the same time you want to have the tools that improve your coding plugged in the project. Setup can take time if you just hit File -> New Project. So naturally like any developer I decided to capture that setup and reuse it whenever I needed.
I started with simple requirements - a modern Android project, Compose UI and navigation, Material 3 for theming including icons and Google fonts. I wanted that “new car smell”, so I did not bother much about backwards compatibility. Minimum SDK set at 31 (Android 12) gives conservatively 70% device coverage and clears away most compatibility warnings and platform dependent code that we normally have to deal with in our production development.
While I don’t know what kind of project will be built on top of that template, I’m sure that testing needs to be a major part of any project’s development process. So here come testing facilities - bright and shiny JUnit5 with MockK, Turbine and Robolectric to cover most Android project testing requirements.
Next on my list were code hygiene tools - strict linter and code analysis with Ktlint and Detekt orchestrated by Spotless will help keep focus and speed.
Last but not least by importance are DI with Koin and logging with Timber.
The template is available on GitHub, README explains how to use it. There is a shell script that replaces package name and app name for your convenience.
Building this template brought my attention to a couple of technical nuances and made me think about some architectural solutions that we deal with while working on code infrastructure no matter how small or big the repository is. I’m not going to explain how to use Gradle or MockK or any other tools, there’s enough documentation already. I’m going to pinpoint a few tricky parts that I encountered though.
Build System
Version catalog (gradle/libs.versions.toml) is the single source of truth that defines external dependencies like libraries and plugins for the project. Most important this is the place where versions are defined. Renovate bot scans default version catalog and Gradle related files like settings.gradle.kts or build.gradle.kts to find where those versions are defined. It checks periodically for possible upgrades and will open a PR to bump the version if needed. It requires some setup and connection between Renovate and your GitHub repository and a short configuration renovate.json.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"labels": ["dependencies"],
"packageRules": [
{
"matchManagers": ["gradle"],
"groupName": "Gradle / AndroidX",
"groupSlug": "gradle"
},
{
"description": "Keep KSP aligned with Kotlin — review together",
"matchPackageNames": ["org.jetbrains.kotlin.android", "com.google.devtools.ksp", "org.jetbrains.kotlin.plugin.compose"],
"groupName": "Kotlin + KSP"
}
]
}
Using latest Gradle version build system follows ‘convention plugin’ setup. Build configuration is set up in code defined in a separate module build-logic modelled after Now In Android project. There are 2 plugins defined, one for Android app module and another one for Android libraries. Library plugin is not used anywhere in the project, it’s for future use. I think it’s enough for a template project. Of course when project grows you might want to add more plugins for specific modules like feature module or data layer with persistence mechanism but at this stage there are no constraints for any necessary toolset or dependencies.
1
2
3
4
5
6
7
8
9
10
11
12
13
AndroidTemplate/
├── app/
│ └── src/
│ ├── main/
│ ├── test/
│ └── androidTest/
├── build-logic/
│ └── convention/
│ └── src/main/kotlin/
│ ├── AndroidApplicationConventionPlugin.kt
│ ├── AndroidLibraryConventionPlugin.kt
└── gradle/
└── libs.versions.toml
Having configuration code as Kotlin modules rather than kts scripts is way more handy for a developer like me. It allows us to organize build configuration the way we want it just like we do with application code. Unlike application code build logic Kotlin files live in default code directory without package declaration. It is done deliberately to make a distinction and so that new project script changes only application module paths.
There were a few small things to notice when using Gradle 9 / AGP 9 that one should be aware when migrating or starting a new project. As of AGP 9 / Gradle 9, the Android plugin pulls Kotlin support in itself; the standalone kotlin.android plugin is no longer required, and applying it breaks the build.
AGP 9 also removed the generic parameterization of CommonExtension; DSL blocks like lint {} only exist on the concrete ApplicationExtension / LibraryExtension types now. Don’t try to add them back into the shared configureKotlinAndroid() in KotlinAndroid.kt — it’ll fail to compile. Configure them per-plugin instead, as AndroidApplicationConventionPlugin / AndroidLibraryConventionPlugin already do for lint {}. That results in some code duplication but it is acceptable for the purpose of having both plugins configured independently.
Kotlin is now a dominant language for Android development, Java is only present in legacy modules. I think it’s fair if the source set paths reflected that.
1
2
3
4
5
6
7
8
commonExtension.apply {
sourceSets.all {
// First the default paths like "src/main/java" should be cleared
kotlin.directories.clear()
// Add new paths injecting source set name in the path
kotlin.directories.add("src/$name/kotlin")
}
}
JVM configuration is important and it’s set in build-logic/convention/build.gradle.kts. Notice that both java and kotlin configurations have to match one another but they use different enumerations.
1
2
3
4
5
6
7
8
9
10
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
The Stack
Dependency Injection: Koin is my preferred choice for dependency injection. Like Dagger and Hilt it supports JSR-330 but it is Kotlin Multiplatform compatible and it is easy to use in Android code.
Testing: Writing concise and expressive tests is one sharp tool in developer’s toolbox. Template has everything required to do so in Android project. Apart from common AndroidX and KotlinX coroutines testing wirings it has Robolectric to mimic Android infrastructure in your local environment. MockK is an excellent mock library for Kotlin specifically. Turbine is a small library that makes testing flows easy and expressive. Finally it is all run by latest JUnit 5.
There are a few gotchas when using JUnit 5 that are not obvious. First one is that when running instrumented tests current JUnit 6.x requires API 35+ on emulators. Nothing will point in this direction in the IDE it just fails with NoSuchMethodError. To mitigate this JUnit version can be downgraded to 5.13.4 (current 5.x version) in version catalog.
Another point to note is that testImplementation and androidTestImplementation are completely different configurations. So when you declare the same platform dependency for both you might get a misleading warning about “dependency platform declared multiple times”. This might pop up in build files if you use standard build.gradle.kts for your module or convention plugin. Convention plugins as Kotlin classes don’t have this issue.
One last thing - Compose testing requires JUnit 4 ui-test-junit4. It is run by JUnit 5 vintage engine so it supports annotations from both unit test styles.
Code Quality: I have a strong conviction that when starting a new project or even hacking down a prototype developer needs to strive to produce the best quality code possible. Too many times I saw prototype code borrowed into production without proper review. Good thing there are automated tools that help. They will even format the code for you wherever it is possible. Pipeline is there to smooth the ride and not to choke coding creativity.
It starts with Spotless configured to run Ktlint and Detekt. Detekt is only used for static analysis and not formatting so that it does not fight Ktlint. All warnings are treated as errors that will fail the build and there’s no baseline since this is a new project. That will allow to write good code from day one. To make it automatic a git hook is configured to run spotlessApply task on every commit and if you want IDE integration Ktlint has a community plugin that works well.
One particular thing I wanted to mention is a setting that differs from convention.
1
2
3
4
5
6
[*]
indent_style = space
indent_size = 8
[*.{kt,kts}]
max_line_length = 90
Max line length is set shorter than most accepted 100-120 (slightly longer than classic 80) and indent size is double the standard 4 spaces. This is a deliberate choice on my side. Of course as a hired developer I comply with code styles accepted by the team but I believe that having shorter lines and larger indents helps writing readable code by forcing developers have less nested blocks per function, shorter method chains etc. Yes, modern wide monitors can accommodate long lines but not if you split your screen for a diff view or just to see 2 different files at the same time.
Bootstrapping: There is a shell script that makes starting a new project a little easier: new-project.sh. The only thing it does is to rename the project and top level package. It will work in environments that have bash like Apple and Linux. If you are running on Windows it should be done manually or you can ask your AI agent to generate a similar PowerShell script (that’s what I did).
The Android and JVM ecosystem hands you a choice at every layer: Dagger, Hilt, or Koin for DI; one test runner or another; a dozen small calls like it. At some point you commit, and you settle into a stack you have chosen to master. And like every Android developer I know, I keep spinning up throwaway projects just to try something. A template is where those decisions get recorded once, so a new project starts from my considered defaults instead of the IDE’s generic ones.
And the last point, and probably the real one for this post - creating a template helps to capture technical nuances that you encounter while configuring your build system. It helps us learn and it saves us a little time next time.