【Jetpack Compose进阶学与练】第13课:整套系列综合小项目实战(简易笔记App) 前言前面1‑12课系统学习了Compose基础布局、Modifier、状态、remember、ViewModel、MVI架构、Room本地数据库、Navigation导航、SavedStateHandle、项目分包与单Activity架构。本节课做一个完整小型实战简易笔记App。功能笔记列表展示、新增笔记、查看详情、删除笔记本地Room持久化MVI单向数据流单Activity Navigation‑Compose导航。项目功能清单笔记首页列表页展示全部笔记新增笔记页面笔记详情页面查看、删除Room保存笔记数据重启App数据不丢失MVIIntent‑UiState‑EffectNavigation完成页面跳转前置依赖build.gradleplugins { id com.android.application id org.jetbrains.kotlin.android id kotlin-kapt id(androidx.room) } dependencies { implementation androidx.navigation:navigation-compose:2.8.3 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0 implementation androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0 implementation androidx.room:room-runtime:2.7.0 kapt androidx.room:room-compiler:2.7.0 }完整示例源码importandroidx.room.Daoimportandroidx.room.Databaseimportandroidx.room.Entityimportandroidx.room.Insertimportandroidx.room.PrimaryKeyimportandroidx.room.Queryimportandroidx.room.RoomDatabaseimportandroidx.lifecycle.SavedStateHandleimportandroidx.lifecycle.ViewModelimportandroidx.lifecycle.viewModelScopeimportandroidx.navigation.NavHostControllerimportandroidx.navigation.compose.NavHostimportandroidx.navigation.compose.composableimportandroidx.navigation.compose.rememberNavControllerimportandroidx.navigation.navArgumentimportandroidx.navigation.NavTypeimportkotlinx.coroutines.flow.Flowimportkotlinx.coroutines.flow.MutableStateFlowimportkotlinx.coroutines.flow.SharedFlowimportkotlinx.coroutines.flow.asSharedFlowimportkotlinx.coroutines.flow.launchInimportkotlinx.coroutines.flow.onEachimportkotlinx.coroutines.launchimportandroidx.compose.foundation.layout.Arrangementimportandroidx.compose.foundation.layout.Columnimportandroidx.compose.foundation.layout.fillMaxSizeimportandroidx.compose.foundation.layout.fillMaxWidthimportandroidx.compose.foundation.layout.paddingimportandroidx.compose.foundation.lazy.LazyColumnimportandroidx.compose.foundation.lazy.itemsimportandroidx.compose.material3.Buttonimportandroidx.compose.material3.Cardimportandroidx.compose.material3.OutlinedTextFieldimportandroidx.compose.material3.Textimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.collectAsStateWithLifecycleimportandroidx.compose.runtime.getValueimportandroidx.compose.ui.Modifierimportandroidx.compose.ui.unit.dp// ---------- Room Entity 实体 ----------Entity(tableNamenote)dataclassNote(PrimaryKey(autoGeneratetrue)valid:Int0,valtitle:String,valcontent:String)// ---------- Dao ----------DaointerfaceNoteDao{Query(SELECT * FROM note ORDER BY id DESC)funobserveAll():FlowListNoteInsertsuspendfuninsertNote(note:Note)Query(DELETE FROM note WHERE id:nId)suspendfundeleteById(nId:Int)Query(SELECT * FROM note WHERE id:nId)suspendfunfindById(nId:Int):Note?}Database(entities[Note::class],version1)abstractclassNoteDatabase:RoomDatabase(){abstractfunnoteDao():NoteDao}// ---------- Repository仓库 ----------classNoteRepository(privatevaldao:NoteDao){funobserveAllNotes():FlowListNotedao.observeAll()suspendfunaddNote(note:Note)dao.insertNote(note)suspendfundeleteNote(id:Int)dao.deleteById(id)suspendfungetNoteById(id:Int):Note?dao.findById(id)}// ---------- MVI Intent / UiState / Effect ----------sealedclassNoteIntent{objectLoadList:NoteIntent()dataclassCreateNote(valtitle:String,valcontent:String):NoteIntent()dataclassDeleteNote(valnoteId:Int):NoteIntent()}sealedclassNoteUiState{objectLoading:NoteUiState()dataclassSuccess(vallist:ListNote):NoteUiState()dataclassError(valmsg:String):NoteUiState()}sealedclassNoteEffect{objectNavigateBack:NoteEffect}// ---------- ViewModel ----------classNoteViewModel(privatevalrepo:NoteRepository,savedStateHandle:SavedStateHandle):ViewModel{privateval_uiStateMutableStateFlowNoteUiState(NoteUiState.Loading)valuiState_uiStateprivateval_effectkotlinx.coroutines.flow.MutableSharedFlowNoteEffect()valeffect:SharedFlowNoteEffect_effect.asSharedFlow()init{repo.observeAllNotes().onEach{list-_uiState.valueNoteUiState.Success(list)}.launchIn(viewModelScope)}funsendIntent(intent:NoteIntent){viewModelScope.launch{when(intent){NoteIntent.LoadList-{}isNoteIntent.CreateNote-{repo.addNote(Note(titleintent.title,contentintent.content))_effect.emit(NoteEffect.NavigateBack)}isNoteIntent.DeleteNote-{repo.deleteNote(intent.noteId)_effect.emit(NoteEffect.NavigateBack)}}}}}// ---------- 路由常量 ----------objectAppRoute{constvalNOTE_LISTnote_listconstvalNOTE_ADDnote_addconstvalNOTE_DETAILnote_detail/{noteId}}// ---------- 导航宿主 ----------ComposablefunNoteAppNavHost(navController:NavHostController,vm:NoteViewModel){NavHost(navControllernavController,startDestinationAppRoute.NOTE_LIST){composable(AppRoute.NOTE_LIST){NoteListScreen(navController,vm)}composable(AppRoute.NOTE_ADD){NoteAddScreen(navController,vm)}composable(routeAppRoute.NOTE_DETAIL,argumentslistOf(navArgument(noteId){typeNavType.IntType})){backStackEntry-valnoteIdbackStackEntry.arguments?.getInt(noteId)?:-1NoteDetailScreen(navController,vm,noteId)}}}// ---------- 笔记列表页面 ----------ComposablefunNoteListScreen(navController:NavHostController,vm:NoteViewModel){valuiStatebyvm.uiState.collectAsStateWithLifecycle()Column(modifierModifier.fillMaxSize().padding(16.dp)){Button(onClick{navController.navigate(AppRoute.NOTE_ADD)}){Text(➕新建笔记)}when(uiState){NoteUiState.Loading-Text(加载中……)isNoteUiState.Success-{vallist(uiStateasNoteUiState.Success).list LazyColumn{items(list,key{it.id}){item-Card(modifierModifier.fillMaxWidth().padding(vertical4.dp)){Column(modifierModifier.padding(12.dp)){Text(textitem.title)Button(onClick{navController.navigate(note_detail/${item.id})}){Text(查看详情)}}}}}}isNoteUiState.Error-Text((uiStateasNoteUiState.Error).msg)}}}// ---------- 新增笔记页面 ----------ComposablefunNoteAddScreen(navController:NavHostController,vm:NoteViewModel){varinputTitlebyandroidx.compose.runtime.remember{mutableStateOf()}varinputContentbyandroidx.compose.runtime.remember{mutableStateOf()}androidx.lifecycle.LifecycleEffect(androidx.lifecycle.Lifecycle.Event.ON_RESUME){vm.effect.collect{if(itNoteEffect.NavigateBack){navController.popBackStack()}}}Column(modifierModifier.fillMaxSize().padding(20.dp),verticalArrangementArrangement.Top){Text(新建笔记)OutlinedTextField(valueinputTitle,onValueChange{inputTitleit},label{Text(标题)},modifierModifier.fillMaxWidth())OutlinedTextField(valueinputContent,onValueChange{inputContentit},label{Text(内容)},modifierModifier.fillMaxWidth())Button(onClick{vm.sendIntent(NoteIntent.CreateNote(inputTitle,inputContent))}){Text(保存笔记)}Button(onClick{navController.popBackStack()}){Text(取消)}}}// ----------笔记详情页面 ----------ComposablefunNoteDetailScreen(navController:NavHostController,vm:NoteViewModel,noteId:Int){//真实项目这里应当从仓库查询单条笔记androidx.lifecycle.LifecycleEffect(androidx.lifecycle.Lifecycle.Event.ON_RESUME){vm.effect.collect{if(itNoteEffect.NavigateBack){navController.popBackStack()}}}Column(modifierModifier.fillMaxSize().padding(20.dp)){Text(笔记详情 id:$noteId)Button(onClick{vm.sendIntent(NoteIntent.DeleteNote(noteId))}){Text(删除笔记)}Button(onClick{navController.popBackStack()}){Text(返回列表)}}}ComposablefunNoteAppRoot(vm:NoteViewModel){valnavControllerrememberNavController()NoteAppNavHost(navController,vm)}逐行解读整体架构回顾MVI分层UI层各个Screen页面收集UiState渲染界面响应用户点击发送Intent给ViewModel监听Effect处理一次性导航跳转。ViewModel层接收Intent调用Repository更新UiState发出Effect页面回退。Repository层协调Room数据源上层ViewModel不直接访问Dao。Data层Room Entity‑Dao‑DatabaseDao返回Flow数据库变更自动推送列表页面自动刷新。导航Navigation‑Compose三个页面路由笔记列表、新增笔记、笔记详情携带路径参数noteId回退栈管理页面使用统一AppRoute常量管理路由。Room的Flow自动订阅repo.observeAllNotes() 在ViewModel的init里面直接收集数据库只要新增或者删除笔记Flow下发新列表更新UiState页面无需手动下拉刷新。MVI当中EffectSharedFlow处理一次性事件Effect用来做页面回退导航注意SharedFlow一次性事件不要使用StateFlowStateFlow会重复消费页面重新进入的时候会再次触发导航。工程层面本项目采用单Activity架构MainActivity里面setContent直接调用NoteAppRoot所有页面全部交给NavHost管理。本示例省略了Database实例初始化正式开发在Application中初始化NoteDatabase再构造Repository再构造ViewModel。容易踩坑❌ Effect一次性事件使用StateFlow页面重新进入的时候重复执行跳转优先SharedFlow。❌ 在Compose直接调用Dao数据库跳过Repository层破坏分层架构。❌ 忘记给LazyColumn items设置key{it.id}删除笔记的时候列表出现闪烁错乱。❌ 在ViewModel内部初始化RoomDatabase数据库实例应当Application全局单例。✅ Dao返回Flow监听数据表MVI单向数据流Navigation‑Compose管理页面单Activity架构。多元化习题练习习题1【基础‑改错题】笔记App里面处理页面回退导航的Effect使用StateFlow会带来什么问题答案StateFlow保存上一次的值当页面重新进入从详情页返回新增页会再次消费旧Effect事件会意外再次触发页面回退一次性事件应当选择SharedFlow。解读StateFlow适合常驻UI状态SharedFlow适合一次性Effect。习题2【基础‑代码修改题】现在笔记详情页面增加修改笔记功能请新建对应的Intent密封类条目。答案dataclassUpdateNote(valnoteId:Int,valtitle:String,valcontent:String):NoteIntent()解读MVI所有用户交互动作统一封装IntentViewModel的sendIntent当中补充when分支处理。习题3【进阶‑补充代码题】简述为什么ViewModel的init里面直接收集repo.observeAllNotes()不需要手动下拉刷新列表。答案Room Dao返回Flow底层监听note数据表只要执行insert或者deleteRoom自动重新执行查询SQLFlow下发最新列表ViewModel收到数据更新UiStateUI层collectAsStateWithLifecycle之后页面自动刷新列表不需要手动调用刷新函数。解读这是RoomFlow响应式的优势。习题4【综合‑逻辑应用题】梳理本笔记App完整数据流用户点击保存笔记按钮直到列表页面看到新增笔记写出完整链路。答案UI(新增笔记页面)点击保存 → sendIntent(CreateNote)发送意图给到ViewModel → ViewModel调用Repository.addNote()写入Room数据库ViewModel发出Effect(NavigateBack)页面执行popBackStack回到列表Room表发生插入observeAll()的Flow收到新的数据更新UiState列表页面collect之后UI自动渲染出刚刚新增的笔记。解读完整MVI单向数据流链路。习题5【概念简答题】在这套笔记实战项目当中Repository层职责是什么答案Repository隔离ViewModel与Room Dao封装数据读写未来如果需要接入网络同步笔记只需要修改Repository内部上层ViewModel几乎不用改动实现业务层与数据源解耦。解读Repository作为数据中间层。本节课知识点总结综合实战简易笔记App覆盖整套课程知识点Compose UI、LazyColumn列表、ViewModel、SavedStateHandle、MVI架构、Room响应式Flow、Navigation‑Compose导航、单Activity架构。MVIIntent封装用户交互UiState常驻页面状态SharedFlow实现Effect一次性事件导航、Toast禁止StateFlow处理一次性事件。Room Dao返回Flow实现响应式数据修改自动下发UI自动刷新列表不需要手动调用刷新。Repository隔离上层业务与底层数据源便于后续扩展网络同步。项目遵循单Activity架构统一NavHost管理全部页面路由使用常量对象维护。下一课预告进阶第14课系列收尾复习总结Compose项目常见坑点汇总学习路线与后续学习方向