Kotlin 与 Android
Kotlin Android 开发主线:ViewModel + StateFlow 状态管理、生命周期安全协程、Compose UI、权限与后台任务。
前置知识
建议先阅读以下内容再进入本文:
概述
Kotlin 是 Android 官方推荐的开发语言。自 2019 年 Google 宣布 Kotlin 为 Android 首选语言(Kotlin-first)以来,几乎所有新项目都使用 Kotlin 开发,Jetpack 各组件的新 API 均以 Kotlin 优先设计。Kotlin 的空安全、协程、扩展函数与 Flow,恰好对应 Android 开发最痛的四个点:崩溃、回调地狱、样板代码与状态管理。
本文介绍 Kotlin 在 Android 开发中的核心用法:View 与 Compose 两种 UI 方式、ViewModel + StateFlow 状态管理、生命周期安全的协程收集,以及权限、通知、后台任务等高频场景。
基础概念
- Activity:Android 应用的单个屏幕,用户交互的入口
- Fragment:Activity 中的模块化 UI 片段,可复用
- ViewModel:存储和管理 UI 相关的数据,在配置变更(如旋转屏幕)时保留
- Lifecycle:Android 组件的生命周期;
lifecycleScope/viewModelScope让协程随之自动取消 - Jetpack Compose:声明式 UI 工具包,Android 官方推荐的现代 UI 方案
- Context:Android 环境的上下文对象,访问资源和系统服务
快速上手
方式一:View 体系(findViewById / ViewBinding)
// MainActivity.kt
package com.example.myapp
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 找到视图并设置点击事件
val button = findViewById<Button>(R.id.my_button)
button.setOnClickListener {
Toast.makeText(this, "按钮被点击", Toast.LENGTH_SHORT).show()
}
}
}
方式二:Jetpack Compose(新项目默认)
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
GreetingScreen()
}
}
}
}
@Composable
fun GreetingScreen() {
var count by remember { mutableStateOf(0) } // remember:重组间保留状态
Column(modifier = Modifier.padding(16.dp)) {
Text("点击次数: $count")
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
两种方式可以在同一项目共存;新项目建议直接 Compose。构建配置(Compose 编译器插件)见 Kotlin 与 Gradle,Compose 的深入用法见 Kotlin 与 Compose。
详细用法
ViewModel 和 StateFlow:单向数据流
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
// 定义 UI 状态(不可变 data class)
data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
class UserViewModel(private val repository: UserRepository) : ViewModel() {
// 内部可变,外部只读:把更新入口收敛到 ViewModel
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = repository.getUser(userId) // suspend 网络调用
_uiState.update { it.copy(isLoading = false, user = user) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}
Compose 中收集这个状态(生命周期安全):
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun UserScreen(viewModel: UserViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
when {
state.isLoading -> LoadingIndicator()
state.error != null -> ErrorMessage(state.error!!)
state.user != null -> UserCard(state.user!!)
}
}
协程与生命周期:repeatOnLifecycle 是正解
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.*
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 一次性任务:lifecycleScope,Activity 销毁时自动取消
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { apiService.fetchData() }
updateUI(data) // withContext 结束后自动回到主线程
}
// 持续收集流:repeatOnLifecycle,STARTED 开始收集、STOPPED 取消
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> render(state) }
}
}
}
}
注意:launchWhenStarted 已被官方废弃——它只是”挂起等待”,收集者仍然存活;repeatOnLifecycle 则真正取消收集,避免后台时白白处理更新。旧教程中出现它时应替换。
Intent 和导航
// 启动另一个 Activity
fun navigateToDetail(userId: String) {
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("USER_ID", userId)
}
startActivity(intent)
}
// 在目标 Activity 中获取参数
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId = intent.getStringExtra("USER_ID") ?: return
// 使用 userId 加载数据
}
}
// Compose 中使用 Navigation Compose
@Composable
fun NavGraph() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(
onUserClick = { id -> navController.navigate("detail/$id") }
)
}
composable(
"detail/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId") ?: ""
DetailScreen(userId)
}
}
}
权限请求
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
class CameraActivity : AppCompatActivity() {
// 注册权限请求(必须在 STARTED 之前注册,通常作为字段)
private val requestPermission = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
openCamera()
} else {
Toast.makeText(this, "需要相机权限", Toast.LENGTH_SHORT).show()
}
}
fun checkAndRequestCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED
) {
openCamera()
} else {
requestPermission.launch(Manifest.permission.CAMERA)
}
}
}
常见场景
网络请求与错误处理
class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
private val _news = MutableStateFlow<List<News>>(emptyList())
val news: StateFlow<List<News>> = _news.asStateFlow()
fun loadNews() {
viewModelScope.launch {
try {
_news.value = repository.getNews()
} catch (e: retrofit2.HttpException) {
// 服务器错误(4xx/5xx):提示用户稍后重试
} catch (e: java.io.IOException) {
// 网络错误:提示检查网络
}
}
}
}
本地数据存储:DataStore
import android.content.Context
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
// 顶层扩展属性:全应用单例的 DataStore
val Context.dataStore by preferencesDataStore(name = "settings")
class SettingsManager(private val context: Context) {
private object Keys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val FONT_SIZE = intPreferencesKey("font_size")
}
// 读取设置:DataStore 返回 Flow,配置变化自动通知
val darkMode: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[Keys.DARK_MODE] ?: false
}
// 保存设置:edit 是挂起函数,内部保证原子写
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[Keys.DARK_MODE] = enabled
}
}
}
通知:先建渠道再发送
Android 8.0(API 26)起通知必须归属 NotificationChannel,否则不会显示:
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
fun showNotification(context: Context, title: String, message: String) {
val channelId = "general"
val manager = context.getSystemService(NotificationManager::class.java)
// 1. 创建渠道(幂等,重复创建无害)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(channelId, "常规通知", NotificationManager.IMPORTANCE_DEFAULT)
)
}
// 2. 构建并发送(Android 13+ 还需 POST_NOTIFICATIONS 运行时权限)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
NotificationManagerCompat.from(context).notify(1, notification)
}
注意事项与常见陷阱
- 不要在 ViewModel 中持有 Activity/View 引用:配置变更后旧 Activity 需要被回收,被 ViewModel 抓住就泄漏。需要 Context 时用
AndroidViewModel(application)拿 application 级 Context。 - 主线程不能做耗时操作:网络、数据库走
Dispatchers.IO,重计算走Dispatchers.Default;Room 与 Retrofit 的 suspend 接口已内部处理,直接挂起调用即可。 - 用
repeatOnLifecycle收集流:lifecycleScope.launch { flow.collect {} }在后台仍会收集,浪费电且可能在 STOPPED 状态更新 UI;launchWhenStarted已废弃。 by viewModels()/by activityViewModels():前者 Activity 私有,后者跨 Fragment 共享;混用会导致”数据怎么没了”。- 字符串资源:不要硬编码字符串,使用
getString(R.string.xxx)以支持多语言;Compose 中用stringResource(R.string.xxx)。 - 避免在 onDraw 中创建对象:自定义 View 的 onDraw 会被频繁调用,在其中创建对象会造成 GC 压力与掉帧。
进阶用法
Hilt 依赖注入
官方推荐的 DI 方案(编译期生成);轻量替代品 Koin 见 Kotlin 与 Koin。
// Application 类
@HiltAndroidApp
class MyApp : Application()
// Activity 注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository
private val viewModel: UserViewModel by viewModels()
}
// Module 提供依赖
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideUserRepository(api: ApiClient): UserRepository {
return UserRepositoryImpl(api)
}
}
WorkManager 后台任务
import androidx.work.*
class SyncWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
return try {
repository.syncData() // suspend,挂起执行后台同步
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
fun scheduleSync(context: Context) {
val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
注意系统对周期任务的最小间隔限制(15 分钟);需要精确闹钟类场景用 AlarmManager,而不是 WorkManager。
小结
- 现代 Android Kotlin 开发的标准分层:Compose/View 收集
StateFlow-> ViewModel 持有状态 -> Repository + suspend/Flow 数据源。 - 生命周期与协程的三件套:
viewModelScope(随 ViewModel)、lifecycleScope(随组件)、repeatOnLifecycle(STARTED)(持续收集流)。 launchWhenStarted已废弃;收集 StateFlow/SharedFlow 一律用repeatOnLifecycle或 Compose 的collectAsStateWithLifecycle。- 权限用
registerForActivityResult,通知先建 Channel,存储用 DataStore(返回 Flow),后台任务用 WorkManager。 - UI 深入见 Kotlin 与 Compose,工程构建见 Kotlin 与 Gradle。