Android开发这几年变化太快,如果你还停留在写findViewById、开AsyncTask的年代,出去面试或者接手新项目,大概率会觉得很吃力。Kotlin彻底转正成了Android的一等语言,Jetpack又把这几年Google在架构、生命周期、UI开发上的最佳实践全部收拢成了一套标准方案。这篇文章就围绕"Kotlin & Jetpack"这套现代Android开发组合拳,把里面的核心思路、关键技术点、实操时容易踩的坑,一次讲清楚。
1. 这套组合的底层逻辑:为什么是Kotlin + Jetpack
先聊一个很多人没认真想过的问题:为什么是Kotlin,而不是继续用Java?又为什么是Jetpack,而不是各自为战的开源库?
Kotlin能上位,表面原因是Google力推,深层原因是它确实解决了Java在Android开发里最痛的那几个点。空指针安全问题,通过可空类型和编译器检查,把大量运行期崩溃提前到编译期就挡住;冗长的样板代码,用数据类、扩展函数、lambda表达式压缩掉一半以上;函数式编程的支持,让集合操作、回调处理写起来行云流水。这些不是锦上添花,是实打实影响每天的开发效率和代码质量。
Jetpack要解决的问题更根本。Android系统碎片化严重,不同版本API差异大,生命周期管理混乱,配置变更(比如转屏)导致的数据丢失问题一直是老大难。Jetpack把这些跨版本兼容、生命周期绑定、数据持久化等复杂问题,统一封装成一套组件,让你用标准化的方式去处理。它不是一个库,是一整套解决问题的思路和规范。
这两者结合后效果是1+1大于2的。Kotlin的协程天然适合Jetpack组件里大量的异步场景,lifecycleScope、viewModelScope这些扩展让协程的生命周期自动跟着Activity/Fragment走。空安全特性也让LiveData、Room这类组件在使用上更顺手。整个组合的核心价值就一句话:让你用更少的代码、更清晰的结构、更少的崩溃,写出更好维护的App。
2. Kotlin核心特性:真正用得上的那些语法
光是会说"Kotlin比Java好"没用,关键得知道好在哪、怎么用。这里只说实际项目里最高频的几个特性。
2.1 空安全与不可变:编译期就把坑填平
Java里写String name = null;是家常便饭,但运行到name.length()直接NPE崩溃。Kotlin通过类型系统把"可能为空"和"不能为空"分开:
// 不可空类型,编译器禁止赋值null var name: String = "Hello" // 可空类型,使用时必须做空判断 var nullableName: String? = null // 安全调用,为空则整个表达式为null,不会崩溃 val length = nullableName?.length // Elvis操作符,为空时提供默认值 val len = nullableName?.length ?: 0这套设计在实际开发里最大的价值,不是"你记得判空",而是编译器强迫你判空。写代码的时候,看到类型后面带?,你就知道这里可能有坑,必须处理。这一条规则,就能消灭掉Java项目里至少30%的NPE崩溃。
不可变性是另一个容易被忽略的点。用val声明只读变量,用data class配合不可变集合,配合copy()方法做浅拷贝。多线程环境下,不可变对象天然线程安全,不需要加锁,也减少了状态被意外修改的隐患。
2.2 扩展函数与高阶函数:写DSL一样流畅的代码
扩展函数可以给现有类添加新方法,不用继承也不用修改原类。举个例子,以前写Toast要写一长串,用扩展函数一行搞定:
fun Context.showToast(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } // 在Activity里直接调用 showToast("保存成功")高阶函数(参数是函数或返回函数)配合标准库的let、apply、run、also,能写出很优雅的链式代码:
user?.let { it.name = "新名字" it.age = 20 } ?: run { // user为null时执行 showEmptyView() }apply在初始化对象时特别好用:
val intent = Intent().apply { action = Intent.ACTION_VIEW data = Uri.parse(url) flags = Intent.FLAG_ACTIVITY_NEW_TASK }2.3 协程:把异步从"回调地狱"里解放出来
协程是Kotlin给Android开发带来的最大礼物。它本质上是线程框架的封装,让异步代码可以像同步代码一样顺序书写。看个对比就明白了。
传统回调方式:
// 两个网络请求,第二个依赖第一个的结果 api.getUser { user -> api.getOrders(user.id) { orders -> showOrders(orders) } }协程方式:
lifecycleScope.launch { val user = api.getUser() // 挂起,不阻塞主线程 val orders = api.getOrders(user.id) showOrders(orders) }launch启动一个协程,suspend函数在遇到耗时操作时自动挂起,完成后自动恢复。全程不阻塞主线程,代码还是顺序的,读起来逻辑非常清楚。
协程里的调度器(Dispatchers)控制线程切换:
lifecycleScope.launch(Dispatchers.IO) { // 在IO线程执行 val result = doNetworkRequest() withContext(Dispatchers.Main) { // 切回主线程更新UI textView.text = result } }3. Jetpack核心组件:每个都在解决一个具体问题
Jetpack涵盖的组件非常多,从架构到UI到行为到基础。实际项目里,下面这几个是不能跳过的核心。
3.1 Lifecycle:让组件感知生命周期,不再靠手动管理
在没有Lifecycle之前,你在Activity的onResume里做一件事,必须记得在onPause里做相反的清理。一旦漏了,轻则资源泄漏,重则崩溃。Lifecycle把生命周期事件标准化了:
class LocationObserver : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun startLocation() { /* 开始定位 */ } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun stopLocation() { /* 停止定位,防止耗电 */ } } // 绑定 lifecycle.addObserver(LocationObserver())现在推荐用LifecycleCoroutineScope来配合协程,lifecycleScope.launch启动的协程,会在Lifecycle销毁时自动取消,从根上杜绝协程泄漏:
lifecycleScope.launch { repeat(10) { delay(1000) updateTimer() } // Activity销毁时,这个协程自动取消 }3.2 ViewModel:数据在配置变更后依然健在
转屏是Android开发的经典难题:Activity被销毁重建,Activity里的临时数据全没,你必须在onSaveInstanceState里塞数据再恢复。麻烦,而且只适合小量数据。
ViewModel的设计目标很纯粹:把数据从Activity里拿出来,放到一个独立于Activity生命周期的容器里。Activity旋转销毁时,ViewModel实例不会被销毁,新的Activity通过同一个ViewModel继续拿到旧数据:
class MyViewModel : ViewModel() { val userData = MutableLiveData<User>() fun loadUser() { viewModelScope.launch { userData.value = repository.fetchUser() } } } class MainActivity : AppCompatActivity() { private val viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel.userData.observe(this) { user -> // 更新UI,转屏后这里自动恢复 } } }3.3 LiveData:带生命周期感知的数据持有者
LiveData是一套"观察者模式+生命周期绑定"的实现。核心特性是只在活跃状态时才通知观察者。所谓活跃状态,指Lifecycle处于STARTED之后。
比如你在前台时,数据更新立刻刷新UI;App退到后台,通知停止(UI本来看不见,没必要浪费资源);回到前台自动恢复。这个机制杀死了大量"Activity销毁后还在更新UI导致崩溃"的bug:
viewModel.userData.observe(this) { user -> // 只在Activity处于STARTED状态时回调 // Activity销毁后自动移除观察者,不会泄漏 }3.4 LiveData、Flow和StateFlow怎么选
LiveData简单好用,但在处理复杂异步数据流、数据转换、组合操作时,能力有限。这时候Kotlin的Flow更合适。Flow是响应式数据流,类似RxJava但基于协程,学习门槛更低:
// Flow:处理流式数据、组合变换更强大 fun getAllData(): Flow<Data> = flow { emit(repository.getLocalData()) // 先发本地缓存 emit(repository.getNetworkData()) // 再发网络数据 } // StateFlow:状态持有型数据流,类似LiveData,但代码更干净 val userState = MutableStateFlow<User?>(null) // 收集数据 viewLifecycleOwner.lifecycleScope.launch { viewModel.userState.collect { user -> // 更新UI } }简单的场景用LiveData,涉及复杂流式处理、状态管理、Repository层的数据流,建议直接用Flow/StateFlow。一个项目里两者可以共存,不必强求统一。
3.5 Room:比SQLite好用到不知哪里去的数据库
Room是SQLite的ORM封装,在编译期做SQL检查,发现问题直接编译报错而不留到运行期。三大核心组件:Entity定义表结构、DAO定义数据库操作、Database持有数据库实例。
@Entity(tableName = "users") data class User( @PrimaryKey val id: Int, val name: String, val age: Int ) @Dao interface UserDao { @Query("SELECT * FROM users WHERE age > :minAge") suspend fun getUsersByAge(minAge: Int): List<User> @Insert suspend fun insertUser(user: User) @Query("DELETE FROM users WHERE id = :id") suspend fun deleteById(id: Int) } @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } // Kotlin中获取实例 val db = Room.databaseBuilder( context, AppDatabase::class.java, "app-db" ).build()Room配合Flow使用很丝滑,数据库变化自动通知UI层刷新:
@Query("SELECT * FROM users") fun observeAllUsers(): Flow<List<User>>3.6 DataStore:SharedPreferences的现代替代
SharedPreferences有几个臭毛病:无法保证类型安全、误用apply()容易造成ANR、不支持协程。DataStore用Flow替代了监听器,天然支持协程和类型安全。
val Context.dataStore by preferencesDataStore(name = "settings") val themeMode = dataStore.data.map { preferences -> preferences[KEY_THEME] ?: "light" } // 写入 dataStore.edit { settings -> settings[KEY_THEME] = "dark" }上手成本很低,迁移也很平滑,新项目建议直接用DataStore。
3.7 Hilt:把依赖注入规范化
项目大了以后,手动去创建对象、传依赖很痛苦,代码里到处new XXX(),测试也困难。Hilt是Google官方出的依赖注入框架,基于Dagger,但上手门槛低了很多:
@HiltAndroidApp class MyApplication : Application() @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideApiService(): ApiService { return Retrofit.Builder()...build() } } @AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var apiService: ApiService }构造函数注入是最值得推荐的:
class MainViewModel @Inject constructor( private val repository: UserRepository ) : ViewModel() { // 直接可用了,再不用手动new }4. 架构范式:MVVM在现代Android里的落地姿势
Kotlin和Jetpack提供了工具,但工具怎么组合使用,需要一套架构模式来约束。MVVM(Model-View-ViewModel)是目前Android社区公认的主流实践。
4.1 分层:各司其职的钱包设计
MVVM的分层逻辑可以用一个生活场景来理解:UI层(View)是前台收银员,ViewModel是店铺经理,Repository是仓库管理员,Model是仓库里的货。
- View层:只负责渲染UI、转发用户操作,不做任何逻辑。
- ViewModel层:持有界面的状态(State),接收View的事件,调用Repository获取数据,把结果暴露给View。不持有Context,不操作View,这样才可以独立测试。
- Model/Repository层:数据源统一入口,封装本地数据库(Room)和网络(Retrofit)数据,向上提供干净的数据接口。
4.2 ViewModel+LiveData/StateFlow的核心闭环
看一个加ViewModel层后的完整流程代码:
class UserViewModel @Inject constructor( private val repository: UserRepository ) : ViewModel() { private val _userList = MutableStateFlow<List<User>>(emptyList()) val userList: StateFlow<List<User>> = _userList.asStateFlow() private val _loading = MutableStateFlow(false) val loading: StateFlow<Boolean> = _loading.asStateFlow() fun loadUsers() { viewModelScope.launch { _loading.value = true try { val users = repository.getUsers() _userList.value = users } catch (e: Exception) { // 统一错误处理 } finally { _loading.value = false } } } }UI层通过collect来观察状态:
viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.userList.collect { users -> adapter.submitList(users) } } }repeatOnLifecycle保证仅在STARTED以上状态采集数据,后台时自动暂停,回到前台自动恢复。这是目前推荐的UI订阅范式。
4.3 状态管理:用一个类来表达界面状态
纯粹用多个LiveData/StateFlow来管理不同数据块,界面状态一多就乱。更好的做法是用**一个状态容器(UI State)**来统一表达:
data class UserListUiState( val users: List<User> = emptyList(), val isLoading: Boolean = false, val errorMessage: String? = null, val query: String = "" ) class UserListViewModel : ViewModel() { private val _uiState = MutableStateFlow(UserListUiState()) val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow() fun search(query: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, query = query) } // ...网络请求 } } }UI层只观察一个uiState,任何状态变化(加载中、有数据、有错误、有搜索关键字)都会自动刷新整个界面。
5. 从进程到UI:Modern Compose UI带来的一次范式转变
Jetpack Compose是这几年来Android UI开发最大的一次革命,它完全抛弃了XML布局和findViewById,用声明式UI来构建页面。
5.1 Compose的核心写法:状态决定界面
传统View体系是命令式:通过setText、setVisibility等来操作View,数据变了你要手动改每个控件的状态。Compose是声明式:你描述"数据长什么样,界面就是什么样",数据变了,界面自动重组(Recomposition):
@Composable fun UserProfile(user: User) { Column(modifier = Modifier.padding(16.dp)) { Text( text = user.name, style = MaterialTheme.typography.titleLarge ) Text( text = "年龄:${user.age}", style = MaterialTheme.typography.bodyMedium ) Button(onClick = { /* 点按钮做什么 */ }) { Text("编辑") } } }@Composable标记函数就是声明UI的地方。没有XML,没有inflate,没有findViewById,一个函数看代码就知道界面长成什么样。
5.2 状态与重组:记住数据的正确姿势
Compose里最常见的坑是状态丢失和无限重组。用remember保留状态,用mutableStateOf让状态变化触发重组:
@Composable fun Counter() { var count by remember { mutableStateOf(0) } Column { Text("当前计数:$count") Button(onClick = { count++ }) { Text("累加") } } }如果状态需要跨重组保留到配置变更后,就用rememberSaveable:
var count by rememberSaveable { mutableStateOf(0) }5.3 与ViewModel的配合:Compose式状态订阅
Compose配合ViewModel是Android现代开发的黄金组合,用collectAsStateWithLifecycle来订阅状态:
@Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> LoadingIndicator() uiState.errorMessage != null -> ErrorView(uiState.errorMessage) else -> UserList(uiState.users) } }注意UI状态和Compose状态的同步是自动的,ViewModel里的数据一变,界面立刻跟着变。这比传统的observe+notifyDataSetChanged简单太多了。
6. 一个小型完整的现代Android模块实现:从构建到落地的全记录
理论讲了半天,不如一个完整的小项目来得实在。我自己实践过程中做过一个模拟项目X,实现一个简单的新闻App,包含网络请求、本地缓存、状态管理、UI展示,第一阶段经历了从传统写法到现代写法的完整改造,有很多值得说的东西。
6.1 工程环境和依赖配置
项目用Gradle Kotlin DSL来管理依赖,核心依赖如下:
// build.gradle.kts (Module) plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt") id("dagger.hilt.android.plugin") } dependencies { // Compose implementation(platform("androidx.compose:compose-bom:2024.05.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") // ViewModel & Lifecycle implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.1") // Hilt implementation("com.google.dagger:hilt-android:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") kapt("com.google.dagger:hilt-compiler:2.51.1") // Retrofit + OkHttp implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.retrofit2:converter-gson:2.11.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // Room implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") kapt("androidx.room:room-compiler:2.6.1") // DataStore implementation("androidx.datastore:datastore-preferences:1.1.1") }这段配置基本就是现代Android项目的标准起手式。Compose统一UI,Hilt统一依赖,Room本地缓存,Retrofit网络请求,DataStore存偏好设置。
6.2 构建Repository层:统一数据源入口
Repository层的职责是屏蔽数据来源(网络还是本地缓存),对上层(ViewModel)只提供干净统一的数据接口:
interface NewsRepository { fun getLatestNews(): Flow<List<News>> fun getCachedNews(): Flow<List<News>> } class NewsRepositoryImpl @Inject constructor( private val newsApi: NewsApi, private val newsDao: NewsDao ) : NewsRepository { override fun getCachedNews(): Flow<List<News>> { return newsDao.observeAll() } override fun getLatestNews(): Flow<List<News>> { return flow { // 先发缓存,再发网络,两者兼得 emit(newsDao.getLocal()) val remote = newsApi.fetchNews() newsDao.updateAll(remote) emit(remote) }.catch { e -> // 网络失败时,缓存在兜底 emit(newsDao.getLocal()) } } }网络失败时崩溃还是兜底?这就是catch操作符存在的意义。通过这样的设计,用户弱网环境下依然能看到内容。
6.3 ViewModel层:持有状态,驱动数据
@HiltViewModel class NewsViewModel @Inject constructor( private val repository: NewsRepository ) : ViewModel() { private val _uiState = MutableStateFlow(NewsUiState()) val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow() init { loadNews() } fun loadNews() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } repository.getLatestNews().collect { news -> _uiState.update { it.copy(newsList = news, isLoading = false) } } } } data class NewsUiState( val newsList: List<News> = emptyList(), val isLoading: Boolean = false, val errorMessage: String? = null ) }6.4 Compose UI层:响应式订阅状态
@Composable fun NewsScreen(viewModel: NewsViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Scaffold(topBar = { TopAppBar(title = { Text("每日新闻") }) }) { padding -> if (uiState.isLoading && uiState.newsList.isEmpty()) { CircularProgressIndicator(modifier = Modifier.padding(padding)) } else { LazyColumn(modifier = Modifier.padding(padding)) { items(uiState.newsList, key = { it.id }) { news -> NewsCard(news) } } } } }key参数指定唯一ID,Compose在做列表diff时才不会重复创建和错乱。这个小细节在数据频繁更新时非常关键。
6.5 数据库和网络模块的Hilt装配
@Module @InstallIn(SingletonComponent::class) object DataModule { @Provides @Singleton fun provideApi(): NewsApi { return Retrofit.Builder() .baseUrl("https://example-server.com/") .client(OkHttpClient.Builder().build()) .addConverterFactory(GsonConverterFactory.create()) .build() .create(NewsApi::class.java) } @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, "news.db") .build() } @Provides @Singleton fun provideNewsDao(db: AppDatabase): NewsDao = db.newsDao() }整套依赖关系清清楚楚:ViewModel依赖Repository,Repository依赖DAO和API,全部由Hilt在编译期生成并注入。测试的时候想替换成Mock版本,直接改Module即可,侵入性很小。
7. 避坑指南:从崩溃和卡顿中总结出的实战经验
做了一段时间现代Android开发,踩过挺多坑。把最典型的整理出来,帮后来者省点时间。
7.1 处理可空类型时,别偷懒用过度断言
Kotlin里可以强制断言非空(!!),滥用会在运行时炸出类似"NullPointerException at line 32"这种最恶心的崩溃。现实情况是,你在验证过的场景里可以放心用!!,但凡是数据来自网络、数据库、外部输入,一律用安全调用或者?:兜底。
我的经验法则是:能用?:给默认值的就默认值,不能用默认值就显式抛异常并带上上下文信息,比如checkNotNull(data) { "加载用户数据时返回null" }。这比裸!!可读性和排查性都强太多。
7.2 配置变更不是只有转屏
很多人的"配置变更"知识还停留在屏幕旋转上。实际上系统语言切换、浅深色模式切换、字体大小调整、分屏模式变化都会触发Activity重建。ViewModel原理上就是为了抗住这一切而设计的,但你必须注意:ViewModel里不要持有Activity/Fragment的Context或View引用,否则内存泄漏比传统写法更严重。要Context,用AndroidViewModel或者通过注入拿到ApplicationContext。
7.3 协程取消与资源清理的时机
lifecycleScope.launch会自动取消,但取消不等于立刻停止。协程取消是协作式的——在suspend点检查取消状态并抛出CancellationException。如果你在finally里做清理工作,要注意:
viewModelScope.launch { try { repository.saveData(data) } finally { withContext(NonCancellable) { // 这里不会被取消,适合做必须完成的清理 } } }如果清理工作不需要完备保证,直接放finally就行。别在协程里用Thread.sleep,要用delay,否则协程无法响应取消,会出现协程"取消不了"的假象。
7.4 Flow的冷热流,别搞混
Flow分冷热流。冷流(flow{}构建的)是每次有收集者时,生产代码才执行;热流(StateFlow、SharedFlow)是数据由上游独立产生,多个收集者共享同一份数据。
很多初学者在Repository层返回flow { ... }(冷流),界面层每次重组都重新收集,导致重复请求。解决方法是把数据流用stateIn/stateInWhileSubscribed转成热流托管起来:
val newsList = repository.getLatestNews() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() )WhileSubscribed(5000)的意思是:没有收集者5秒后自动停止上游生产,减少资源浪费。
7.5 Compose重组引发的性能陷阱
Compose的声明式写起来爽,但有几处性能雷区。一个是避免在Composable函数里创建大对象,比如把remember忘掉,每次重组都new一堆数据:
// 错误写法 @Composable fun BadExample() { val list = (1..100000).toList() // 每次重组都重新创建 LazyColumn { items(list) { ... } } } // 正确写法 @Composable fun GoodExample() { val list = remember { (1..100000).toList() } LazyColumn { items(list) { ... } } }另一个是避免在onEach里频繁更新状态。如果状态以每秒几十次的频率变化,Compose的重组会非常频繁,导致界面掉帧。这时候要考虑用MutableStateFlow合并更新,而不是用多个mutableStateOf分散触发。
7.6 混合使用传统View和Compose时的注意事项
存量项目不可能一夜间全转Compose,大多是Fragment里嵌入部分Compose或者Compose里嵌套AndroidView。这种混合模式下,最要注意Context的获取方式不同:Compose里用LocalContext.current,别试图在Composable上层持有Activity引用,容易生命周期错乱。
性能上也要小心。Compose和传统View的混合布局,在滚动列表里反复切换会出现measure摩擦,滚动流畅度有明显下降。我的建议是:一个列表页面尽量保持单一实现方式,不要在LazyColumn里混合使用传统RecyclerView和Compose item。
8. 工具链与调试:开发体验里面的大文章
好的工具链能显著提升开发效率,这块在现代Android开发里分量很重。
8.1 代码风格与约束
Kotlin官方推荐使用ktlint或detekt来统一代码风格、检查复杂度。我在项目里用ktlint作为Gradle任务挂在check阶段,不符合规范直接构建失败,强制团队统一风格。同时开启kotlin-allopen插件,让某些类默认可继承(省去写open关键字的繁琐)。
8.2 调试网络与数据库
调试网络请求,OkHttp的logging-interceptor必备,但正式包要关掉。调试数据库,推荐用Android Studio内置的Database Inspector,直接在IDE里看表数据、执行SQL,比SQLite官方工具方便多了。
val client = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } }) .build()8.3 自动化测试:不只是嘴上说说
Kotlin + Jetpack架构下,测试变得比以前容易太多了。ViewModel是纯Kotlin类且不依赖Android,可以直接用JUnit测试;Room数据库可以用内存数据库做单元测试;Compose UI测试用createAndroidComposeRule模拟点击、断言文本。
@RunWith(JUnit4::class) class NewsViewModelTest { @Test fun `加载新闻_数据为空时_显示空提示`() { val repository = FakeNewsRepository(emptyList()) val viewModel = NewsViewModel(repository) runBlocking { viewModel.loadNews() } assertEquals("暂无内容", viewModel.uiState.value.errorMessage) } }写测试不是为了好看,是给后续重构和加功能的信心。现代Android开发框架已经把可测试性做得很好了,不写测试说不过去。
9. 顺手回答几个高频疑问
总有人问我这几个问题,这里一起答了。
Q1:Kotlin会不会学完过时?
目前Android官方所有文档、示例、新组件默认语言都是Kotlin,Jetpack新API也全是Kotlin-first。退一步说,就算未来有新语言,Kotlin积累的生态(协程、Compose)也不是一两年能被替代的。
Q2:Jetpack组件会不会太多,学不完?
不需要都学。优先掌握:Lifecycle、ViewModel、LiveData/StateFlow、Room、DataStore、Hilt(至少了解)、Compose(现在新项目默认)。其他如Navigation、Paging、WorkManager用到再查,学习曲线没那么陡。
Q3:Compose现在是劝退还是必学?
新项目建议直接上Compose。Google已经把Compose列为新项目的默认UI方案,传统View系统基本维持现状不推新。学习Compose初期会有重组、状态这些东西适应期,但一个月后你会发现再也回不去XML时代了。
Q4:老项目怎么转型?
不要大爆炸式重写。我的经验是:新页面用Compose,老页面保持View,用AndroidView做桥接逐步替换;先引入Kotlin协程改造异步代码;再把数据层抽象成Repository,为ViewModel化做准备。一步一步来,比一次性推倒重来带来的挫败感少很多。
10. 个人体会和一些操作上的建议
我自己的感受是,Kotlin + Jetpack这套组合真正的价值,不是某个新特性有多惊艳,而是它把Android开发引向了一条更可控的轨道。传统Android开发的问题在于:生命周期要自己管、异步要自己调、UI要自己同步、崩溃要自己猜,每一步都充满手工作坊式的不确定性。而现在,很多曾经要靠"经验丰富的老手才能避开的坑",框架层直接帮你填了。
举个小例子。以前做页面,Activity里又写数据加载又写列表适配又写点击处理又写权限申请,代码一多必然乱。现在ViewModel只管状态,UI纯函数式渲染,数据层独立,每个模块职责单一、边界清楚。改需求的时候特别明显——改UI不用碰逻辑,改逻辑不用动UI,新需求来了,多数情况下只是加一个State字段、加一个分支、加一个Repository方法而已。
还有一件事值得说:把协程和Flow学好,是真的能够改变代码风格的。你会发现以前那种接口回调套回调的写法再也下不去手了,因为顺序代码写起来太舒服、读起来太清楚了。这不是什么技术上的优越感,纯粹是人面对复杂事物的本能选择——能简单就不复杂,能清楚就不绕弯。
如果是要独立开发或维护中小体量的App,我的建议是别贪多,先把下面这几块打稳:Kotlin语法和协程、ViewModel+StateFlow的状态管理、Room做本地数据、Compose写UI、Hilt管依赖、Retrofit做网络。这一套拿下来,日常需求的开发效率会非常可观。
等到基础扎实了,再去研究Navigation做页面路由、Paging做列表分页、WorkManager做后台任务调度。整个生态是越用越顺的,因为所有组件都在同一套生命周期和协程体系下协作,搭配起来基本无障碍。
最后给个非常实用的建议:训练自己"先写测试,再写实现"的习惯。哪怕只是给ViewModel写几个核心场景的单元测试,长期收益也远超所花费的时间。逻辑出问题时,有测试兜底和没有测试兜底,排查效率完全是两个层级。现代Kotlin + Jetpack这套架构本来就非常适合测试,不利用起来太可惜了。