kotlin-android

Idiomatic Kotlin patterns for Android development — coroutines, Flow, Jetpack Compose, JUnit/Espresso testing, ktlint, and Gradle build commands. Use when working on Android or Kotlin JVM projects.

Kotlin Android Patterns — dog_stack

Idiomatic Kotlin conventions for Android projects, covering coroutines, Compose, testing, and build toolchain.

Idiomatic Kotlin

Null safety

// ✓ Use ?: for defaults, ?.let for nullable chains
val name = user?.name ?: "Unknown"
user?.let { sendEmail(it.email) }

// ✗ Avoid !! in production
val name = user!!.name

Data classes

// ✓ Value objects as data classes
data class User(val id: UUID, val name: String, val email: String)

// ✓ Copy for immutable updates
val updated = user.copy(name = "Bob")

Coroutines

// ✓ Structured concurrency
class UserRepository(private val api: UserApi) {
    suspend fun getUser(id: UUID): User = withContext(Dispatchers.IO) {
        api.fetchUser(id)
    }
}

// ✓ Flow for reactive streams
fun userUpdates(): Flow<User> = flow {
    while (true) {
        emit(api.fetchUser(userId))
        delay(30_000)
    }
}

Jetpack Compose

@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
    Card(modifier = modifier) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = user.name, style = MaterialTheme.typography.headlineSmall)
            Text(text = user.email, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

Testing

JUnit (unit tests)

class UserRepositoryTest {
    private val api = mockk<UserApi>()
    private val repo = UserRepository(api)

    @Test
    fun `getUser returns user from api`() = runTest {
        val expected = User(UUID.randomUUID(), "Alice", "[email protected]")
        coEvery { api.fetchUser(any()) } returns expected

        val result = repo.getUser(expected.id)

        assertEquals(expected, result)
    }
}

Espresso (UI tests)

@RunWith(AndroidJUnit4::class)
class UserScreenTest {
    @get:Rule
    val rule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun displaysUserName() {
        onView(withId(R.id.user_name)).check(matches(withText("Alice")))
    }
}

Run tests:

./gradlew test                     # unit tests
./gradlew connectedAndroidTest     # instrumented tests (requires device/emulator)
./gradlew testDebugUnitTest        # debug variant only

Build Commands

./gradlew assembleDebug            # debug APK
./gradlew assembleRelease          # release APK
./gradlew bundleRelease            # AAB for Play Store
./gradlew clean                    # clean build outputs
./gradlew dependencies             # show dependency tree

ktlint

ktlint                             # lint
ktlint --format                    # auto-fix
ktlint "src/**/*.kt"               # specific files

Common Pitfalls

PitfallFix
GlobalScope.launchUse viewModelScope / lifecycleScope
runBlocking in Android main threadUse lifecycleScope.launch
Leaking Context in ViewModelNever pass Activity to ViewModel
Missing remember in ComposeWrap state with remember { mutableStateOf(...) }
Hardcoded stringsUse strings.xml resource

Related

  • Rules: rules/kotlin/coding-style.md
  • Agent: language reviewers (general-purpose for Kotlin)