30、综合实战:构建一个完整的MVVM应用,全面运用KTX与Jetpack KTX
终于到了最后一章。
说实话,写到这里我有点感慨。前面29章我们聊了那么多KTX的细节——从lifecycleScope到Flow,从SharedPreferences到Room,从Navigation到DataStore。但说真的,知识点学得再多,不落地到项目里,终究是纸上谈兵。
这一章,我们就来干一件实在的事:用Kotlin + KTX + Jetpack KTX,从零搭一个完整的MVVM应用。我会带着你走一遍我平时做项目的真实流程,把前面讲过的那些API全部串起来。
本章目标:构建一个「用户笔记」App。支持用户登录、查看笔记列表、新增笔记、离线缓存。全面使用KTX和Jetpack KTX优化代码。
30.1 项目架构总览
先画一张图,让你对整个项目有个直观印象。我个人习惯在动手写代码之前,先把架构图画清楚。这样后面写代码的时候,心里有谱,不会跑偏。
你看,整个架构分了三层:UI层、ViewModel层、Repository层。每一层都有对应的KTX组件在背后撑腰。说白了,KTX就是帮我们把那些啰嗦的模板代码砍掉,让每一层都写得干净利落。
30.2 项目依赖配置
先上build.gradle。我建议你用version catalog统一管理版本,这样后续升级依赖会方便很多。
// libs.versions.toml (部分)
[versions]
kotlin = "1.9.22"
coroutines = "1.7.3"
lifecycle = "2.7.0"
room = "2.6.1"
hilt = "2.50"
retrofit = "2.9.0"
datastore = "1.0.0"
[libraries]
# KTX 核心
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.12.0" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
# Room KTX
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
# DataStore KTX
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
# Hilt
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.1.0" }
💡 我的习惯:所有KTX依赖统一放在一个组里,用注释标明用途。这样以后找依赖、升级版本,一目了然。我曾经在一个项目里因为版本号散落各处,升级时漏了一个,结果编译报错查了半天……
30.3 数据层:Room + DataStore + Retrofit
数据层是App的基石。我们先用Room KTX定义DAO,再用DataStore KTX做偏好缓存,最后用Retrofit拉网络数据。
30.3.1 Room DAO 与 KTX
Room KTX最爽的地方是什么?就是DAO方法可以直接挂起,不用写回调。你想想看,以前写Room查询,要么用LiveData,要么写AsyncTask,多麻烦。现在直接suspend搞定。
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
fun getAllNotes(): Flow<List<NoteEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: NoteEntity)
@Delete
suspend fun deleteNote(note: NoteEntity)
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Long): NoteEntity?
}
注意看,getAllNotes()返回的是Flow。这意味着只要数据库里的数据变了,UI层会自动收到通知。这就是Room KTX + Flow的魔力——数据驱动UI,完全不用手动刷新。
30.3.2 DataStore KTX 做偏好缓存
用户登录后,我们需要存一个token。以前用SharedPreferences,代码又臭又长。现在用DataStore KTX,清爽多了。
class UserPreferences(private val dataStore: DataStore<Preferences>) {
companion object {
val TOKEN_KEY = stringPreferencesKey("user_token")
val USER_NAME_KEY = stringPreferencesKey("user_name")
}
val tokenFlow: Flow<String?> = dataStore.data
.map { preferences -> preferences[TOKEN_KEY] }
suspend fun saveToken(token: String) {
dataStore.edit { preferences ->
preferences[TOKEN_KEY] = token
}
}
suspend fun clearAll() {
dataStore.edit { it.clear() }
}
}
⚠️ 注意:dataStore.edit是挂起函数,必须在协程里调用。我见过有人直接在UI线程调,结果崩了。记住,DataStore的所有写操作都是异步的,这是它比SharedPreferences安全的地方。
30.3.3 Repository 层:网络+本地缓存
Repository是数据层的门面。我习惯在这里做「网络优先,本地兜底」的策略。
class NoteRepository @Inject constructor(
private val noteDao: NoteDao,
private val noteApi: NoteApi,
private val userPreferences: UserPreferences
) {
// 从本地数据库获取笔记流
fun getNotesFlow(): Flow<List<Note>> {
return noteDao.getAllNotes().map { entities ->
entities.map { it.toDomainModel() }
}
}
// 刷新笔记:先拉网络,再存本地
suspend fun refreshNotes(): Result<Unit> {
return try {
val token = userPreferences.tokenFlow.first() ?: return Result.failure(Exception("未登录"))
val remoteNotes = noteApi.fetchNotes("Bearer $token")
noteDao.insertNotes(remoteNotes.map { it.toEntity() })
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
这里用到了Flow.first()——KTX提供的扩展函数,可以拿到Flow的第一个值然后取消订阅。很适合这种「取一次值」的场景。
30.4 ViewModel 层:全面拥抱 KTX
ViewModel是MVVM的核心。我们用viewModelScope管理协程,用StateFlow暴露状态,用SharedFlow处理一次性事件。
@HiltViewModel
class NoteViewModel @Inject constructor(
private val noteRepository: NoteRepository,
private val userPreferences: UserPreferences
) : ViewModel() {
// UI状态:用StateFlow
private val _uiState = MutableStateFlow(NoteUiState())
val uiState: StateFlow<NoteUiState> = _uiState.asStateFlow()
// 一次性事件:用SharedFlow
private val _event = MutableSharedFlow<NoteEvent>()
val event: SharedFlow<NoteEvent> = _event.asSharedFlow()
init {
// 进入页面就加载笔记
loadNotes()
}
private fun loadNotes() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
// 先展示本地缓存
noteRepository.getNotesFlow().collect { notes ->
_uiState.update { it.copy(notes = notes, isLoading = false) }
}
}
}
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
val result = noteRepository.refreshNotes()
if (result.isFailure) {
_event.emit(NoteEvent.ShowSnackbar("刷新失败: ${result.exceptionOrNull()?.message}"))
}
_uiState.update { it.copy(isRefreshing = false) }
}
}
}
你看,viewModelScope.launch、StateFlow.update、SharedFlow.emit——全是KTX提供的扩展。没有这些,你得手动写线程切换、状态合并、事件队列……代码量至少翻一倍。
核心要点:ViewModel里不要持有Context,不要直接操作View。所有数据通过Flow暴露,UI层去订阅。这样ViewModel才能做到「生命周期感知」和「配置变更不丢失数据」。
30.5 UI 层:DataBinding + Lifecycle KTX
UI层我们用DataBinding + Fragment。Lifecycle KTX提供了lifecycleScope和repeatOnLifecycle,让我们安全地收集Flow。
class NoteListFragment : Fragment(R.layout.fragment_note_list) {
private val viewModel: NoteViewModel by viewModels()
private var _binding: FragmentNoteListBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentNoteListBinding.bind(view)
// 使用 lifecycleScope 安全收集 Flow
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新UI
binding.progressBar.isVisible = state.isLoading
binding.recyclerView.adapter?.submitList(state.notes)
}
}
}
// 收集一次性事件
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.event.collect { event ->
when (event) {
is NoteEvent.ShowSnackbar -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_SHORT).show()
}
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
这里有个细节:repeatOnLifecycle会在Fragment进入STARTED状态时开始收集,进入STOPPED状态时自动取消。这样就不会在后台浪费资源,也不会因为Fragment销毁后还更新UI而崩溃。
💡 避坑指南:我曾经在项目里直接用lifecycleScope.launch收集Flow,没有用repeatOnLifecycle。结果Fragment退到后台后,Flow还在跑,更新UI时抛异常。后来全部改成repeatOnLifecycle,世界清净了。
30.6 依赖注入:Hilt KTX
Hilt KTX让我们可以轻松注入ViewModel、Repository、DataStore等。配合@HiltViewModel注解,连ViewModel的工厂类都不用写了。
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return context.dataStore
}
@Provides
@Singleton
fun provideNoteApi(): NoteApi {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NoteApi::class.java)
}
}
然后在Activity或Fragment里,直接用by viewModels()或者hiltNavGraphViewModels()获取ViewModel,Hilt会自动注入依赖。
30.7 完整流程串联
好了,我们把所有环节串起来,看看一次「用户打开App → 查看笔记 → 下拉刷新」的完整流程:
- App启动:Hilt初始化,注入所有依赖。
- 进入NoteListFragment:ViewModel的
init块调用loadNotes()。 - 加载本地数据:
noteRepository.getNotesFlow()从Room读取笔记,返回Flow。 - UI更新:Fragment通过
repeatOnLifecycle收集uiState,展示笔记列表。 - 下拉刷新:调用
viewModel.refresh(),从网络拉取最新数据。 - 网络请求:Retrofit发起请求,携带DataStore中存储的token。
- 数据回写:网络数据写入Room,Room的
Flow自动通知UI更新。 - 错误处理:如果网络失败,通过
SharedFlow发送Snackbar事件。
整个过程,没有回调,没有LiveData的粘性问题,没有手动线程切换。全部由KTX和Jetpack KTX在背后默默搞定。
30.8 总结与心得
写到这里,我想说几句心里话。
KTX和Jetpack KTX不是银弹,但它们确实让Android开发变得「更像Kotlin」了。我刚开始用Java写Android的时候,一个简单的网络请求要写AsyncTask、Handler、接口回调……代码又臭又长。现在呢?一个viewModelScope.launch就搞定了。
但工具再好,架构才是根本。MVVM + Repository + DI,这套组合拳打好了,你的App才能经得起迭代。KTX只是帮你把代码写得更优雅,但设计模式、分层思想、数据流向——这些才是真正决定代码质量的东西。
嗯,30章的内容,说多不多,说少不少。如果你能跟着一路写下来,我相信你对KTX的理解已经超过大多数开发者了。以后在项目里看到.asLiveData()、.flowWithLifecycle()、dataStore.edit这些API,你应该能会心一笑——「老朋友了」。
最后送你一句话:写代码就像盖房子,KTX是趁手的工具,MVVM是稳固的框架。工具趁手,框架稳固,你才能盖出经得起风雨的大厦。