Android Development with Kotlin — Usage Guide
Android Development with Kotlin — Usage Guide
1. Introduction
Kotlin is the officially recommended language for Android development. It is concise, null-safe, interoperable with Java, and fully supported by Android Studio, Jetpack libraries, and the Gradle build system. This guide covers the core language features, project setup, common patterns, Jetpack Compose UI, and best practices you will use day to day.
2. Language Essentials
2.1 Variables
val immutable: String = "cannot be reassigned"
var mutable: Int = 0
mutable = 1
// Type inference
val name = "Kotlin" // String
val count = 42 // Int
val pi = 3.14 // Double
Prefer val over var. Use var only when the value must change.
2.2 Null Safety
var nullable: String? = null
var nonNull: String = "always set"
// Safe call
val length: Int? = nullable?.length
// Elvis operator — fallback when null
val safeLength: Int = nullable?.length ?: 0
// Not-null assertion — use sparingly
val forced: Int = nullable!!.length
// Smart cast after null check
if (nullable != null) {
println(nullable.length) // treated as non-null here
}
2.3 Strings
val user = "Ada"
val greeting = "Hello, $user!"
val multiline = """
Line one
Line two
""".trimIndent()
2.4 Collections
val list = listOf(1, 2, 3) // read-only
val mutableList = mutableListOf(1, 2, 3)
val set = setOf("a", "b", "a") // [a, b]
val map = mapOf("key" to "value")
// Functional operations
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
2.5 Control Flow
// if as expression
val max = if (a > b) a else b
// when (replaces switch)
val description = when (x) {
0 -> "zero"
in 1..9 -> "single digit"
is Int -> "integer"
else -> "other"
}
// ranges
for (i in 1..10) print(i)
for (i in 10 downTo 1 step 2) print(i)
3. Functions and Lambdas
fun add(a: Int, b: Int): Int = a + b
// Default and named arguments
fun greet(name: String, greeting: String = "Hello") = "$greeting, $name!"
greet(name = "Ada")
greet("Ada", greeting = "Hi")
// Vararg
fun sum(vararg numbers: Int): Int = numbers.sum()
// Lambda
val square: (Int) -> Int = { x -> x * x }
val shortSquare: (Int) -> Int = { it * it }
// Higher-order function
fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x))
4. Classes and Objects
4.1 Data Classes
data class User(val id: Long, val name: String, val email: String? = null)
val u1 = User(1, "Ada")
val u2 = u1.copy(name = "Grace")
println(u1 == u2) // structural equality
4.2 Regular and Open Classes
open class Animal(val name: String) {
open fun sound(): String = "unknown"
}
class Dog(name: String) : Animal(name) {
override fun sound() = "bark"
}
4.3 Sealed Classes
Useful for representing restricted hierarchies, especially UI state.
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
fun render(state: UiState<String>) = when (state) {
is UiState.Loading -> "loading"
is UiState.Success -> state.data
is UiState.Error -> state.message
}
4.4 Object and Companion
object AppConfig {
const val VERSION = "1.0"
}
class Logger private constructor() {
companion object {
fun create() = Logger()
}
}
4.5 Enum
enum class Direction(val degrees: Int) {
NORTH(0), EAST(90), SOUTH(180), WEST(270)
}
5. Coroutines and Asynchrony
Coroutines are the standard way to handle async work on Android.
import kotlinx.coroutines.*
// Launch a coroutine
GlobalScope.launch {
delay(1000)
println("done")
}
// Suspend function
suspend fun fetchUser(id: Long): User {
delay(500)
return User(id, "Ada")
}
// Structured concurrency
suspend fun loadAll(): List<User> = coroutineScope {
val a = async { fetchUser(1) }
val b = async { fetchUser(2) }
listOf(a.await(), b.await())
}
5.1 Dispatchers
Dispatchers.Main // UI thread
Dispatchers.IO // network / database
Dispatchers.Default // CPU-heavy
5.2 Flow
fun countdown(): Flow<Int> = flow {
for (i in 5 downTo 1) {
emit(i)
delay(200)
}
}
// Collect
countdown().collect { value -> println(value) }
6. Project Setup
6.1 Gradle (Kotlin DSL)
build.gradle.kts (module):
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.example.app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
}
6.2 Application Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".App"
android:label="@string/app_name"
android:theme="@style/Theme.App">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
7. Jetpack Compose UI
Compose is the modern declarative UI toolkit for Android.
7.1 Composable Functions
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.unit.dp
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Composable
fun ProfileCard(name: String, onClick: () -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = name, style = MaterialTheme.typography.headlineSmall)
Button(onClick = onClick) {
Text("View")
}
}
}
7.2 State
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
7.3 Side Effects
@Composable
fun Loader(viewModel: MyViewModel) {
LaunchedEffect(Unit) {
viewModel.load()
}
// ...
}
7.4 Lists
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
ProfileCard(name = user.name, onClick = { /* ... */ })
}
}
}
7.5 Theme
@Composable
fun AppTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = lightColorScheme(),
typography = Typography(),
content = content
)
}
8. ViewModel and State
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val state: StateFlow<UiState<List<User>>> = _state
fun load() {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val users = repository.fetchUsers()
_state.value = UiState.Success(users)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "unknown")
}
}
}
}
8.1 Collecting State in Compose
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
when (val s = state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserList(s.data)
is UiState.Error -> Text("Error: ${s.message}")
}
}
9. Dependency Injection with Hilt
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class App : Application()
class UserRepository @Inject constructor() {
suspend fun fetchUsers(): List<User> = emptyList()
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() { /* ... */ }
10. Networking with Retrofit
import retrofit2.http.GET
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
val api = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
11. Room Database
import androidx.room.*
@Entity
data class UserEntity(
@PrimaryKey val id: Long,
val name: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM UserEntity")
fun observeAll(): Flow<List<UserEntity>>
@Insert
suspend fun insert(user: UserEntity)
}
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
12. Permissions
import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@Composable
fun CameraScreen() {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) { /* open camera */ }
}
Button(onClick = { launcher.launch(Manifest.permission.CAMERA) }) {
Text("Grant camera")
}
}
13. Navigation Compose
import androidx.navigation.compose.*
@Composable
fun AppNav() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "home") {
composable("home") { HomeScreen(onNavigate = { nav.navigate("detail") }) }
composable("detail") { DetailScreen() }
}
}
14. Error Handling
suspend fun safeCall(block: suspend () -> String): Result<String> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
Always rethrow CancellationException to preserve coroutine cancellation.
15. Testing
15.1 Unit Test
import org.junit.Test
import org.junit.Assert.*
class CalculatorTest {
@Test
fun adds_correctly() {
assertEquals(4, Calculator.add(2, 2))
}
}
15.2 Coroutine Test
import kotlinx.coroutines.test.*
import org.junit.Test
class ViewModelTest {
@Test
fun loads_users() = runTest {
val vm = UserViewModel(FakeRepository())
vm.load()
assertEquals(UiState.Success(listOf<User>()), vm.state.value)
}
}
16. Best Practices
- Prefer
valand immutability; usedata classfor models. - Never ignore nullability — let the type system guide you.
- Use coroutines and
Flowinstead of callbacks andThread. - Keep business logic out of composables; put it in
ViewModel. - Use
StateFlowfor UI state and collect it withcollectAsState(). - Scope coroutines to
viewModelScopeorlifecycleScope. - Use
sealed classto model state, results, and one-shot events. - Avoid
GlobalScopein app code. - Enable R8/ProGuard minification for release builds.
- Write tests for ViewModels and repositories; keep composables thin.
17. Common Pitfalls
- Using
!!broadly — prefer?:or explicit null checks. - Blocking the main thread with
Thread.sleepor synchronous I/O. - Forgetting
runTestwhen testing suspend functions. - Holding context references in singletons — risk of leaks.
- Mutating state outside of
MutableStateFlowupdates. - Recomposing too often — hoist stable state and use
remember/derivedStateOf.
18. Quick Reference
| Need | Use |
|---|---|
| Immutable model | data class |
| Restricted hierarchy | sealed class |
| UI state | StateFlow + collectAsState |
| Async work | viewModelScope.launch |
| Streams | Flow / StateFlow |
| DI | Hilt |
| Database | Room |
| Networking | Retrofit + Coroutines |
| Navigation | Navigation Compose |
| Permissions | ActivityResultContracts |
This guide covers the core building blocks of modern Android development with Kotlin and Jetpack Compose. Start with sections 2 through 5 to internalize the language, then build UI with section 7, and wire up data with sections 8 through 11.
- 点赞
- 收藏
- 关注作者
评论(0)