【Jetpack Compose进阶学与练】第8课:项目架构分层实战

发布时间:2026/10/10 2:40:27
【Jetpack Compose进阶学与练】第8课:项目架构分层实战
前言前面依次学习了ViewModel、Flow、Room本地数据库、Retrofit网络。现在需要把这些组件组织起来形成一套清晰的分层架构现代Android Compose项目主流是MVI架构思想区分UI层、业务层、数据层明确数据流方向禁止跨层逆向调用。知识点清单四层分层UI层(Compose) — ViewModel层 — Repository仓库层 — 数据源层(网络Retrofit /本地Room)每层职责以及调用方向只能从上往下依赖禁止下层反向调用上层MVI核心概念Intent(用户意图)、State(页面状态)、Effect(一次性事件)单向完整数据流演示用户点击 → UI发送Intent → ViewModel处理 → Repository → 数据源数据原路返回生成新State驱动页面刷新区分State常驻状态与Effect瞬时事件延续前面SharedFlow的认知完整示例源码importandroidx.lifecycle.ViewModelimportandroidx.lifecycle.viewModelScopeimportkotlinx.coroutines.flow.MutableStateFlowimportkotlinx.coroutines.flow.StateFlowimportkotlinx.coroutines.flow.MutableSharedFlowimportkotlinx.coroutines.flow.SharedFlowimportkotlinx.coroutines.launch// -------- MVI定义页面Intent用户意图 --------sealedclassTodoIntent{dataclassAddTodo(valtext:String):TodoIntent()dataclassDeleteTodo(valid:Int):TodoIntent()objectRefresh:TodoIntent()}// -------- MVI页面UI状态 --------sealedclassTodoUiState{objectLoading:TodoUiState()dataclassSuccess(vallist:ListString):TodoUiState()dataclassError(valmsg:String):TodoUiState()}// -------- MVI一次性Effect事件 --------sealedclassTodoEffect{dataclassShowToast(valmessage:String):TodoEffect()}// -------- Repository仓库层 --------classTodoRepository{//模拟数据源真实项目内部同时调用Retrofit网络、Room本地数据库privatevallocalListmutableListOfString()suspendfunrefresh():ListString{kotlinx.coroutines.delay(800)returnlocalList.toList()}suspendfunaddItem(text:String){kotlinx.coroutines.delay(300)localList.add(text)}suspendfundeleteItem(id:Int){kotlinx.coroutines.delay(300)if(id0idlocalList.size){localList.removeAt(id)}}}// -------- ViewModel层接收Intent更新UiState发送Effect --------classTodoMviViewModel(privatevalrepo:TodoRepository):ViewModel(){privateval_uiStateMutableStateFlowTodoUiState(TodoUiState.Loading)valuiState:StateFlowTodoUiState_uiStateprivateval_effectMutableSharedFlowTodoEffect()valeffect:SharedFlowTodoEffect_effectfunsendIntent(intent:TodoIntent){viewModelScope.launch{when(intent){isTodoIntent.AddTodo-{repo.addItem(intent.text)_effect.emit(TodoEffect.ShowToast(新增待办成功))valnewListrepo.refresh()_uiState.valueTodoUiState.Success(newList)}isTodoIntent.DeleteTodo-{repo.deleteItem(intent.id)_effect.emit(TodoEffect.ShowToast(删除待办成功))valnewListrepo.refresh()_uiState.valueTodoUiState.Success(newList)}TodoIntent.Refresh-{_uiState.valueTodoUiState.Loadingtry{valdatarepo.refresh()_uiState.valueTodoUiState.Success(data)}catch(ex:Exception){_uiState.valueTodoUiState.Error(ex.message?:刷新失败)}}}}}}// -------- UI Compose层 --------importandroidx.compose.runtime.Composableimportandroidx.compose.runtime.collectAsStateWithLifecycleimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.LaunchedEffectimportandroidx.compose.foundation.layout.Columnimportandroidx.compose.foundation.layout.fillMaxSizeimportandroidx.compose.foundation.lazy.LazyColumnimportandroidx.compose.foundation.lazy.itemsIndexedimportandroidx.compose.material3.Buttonimportandroidx.compose.material3.CircularProgressIndicatorimportandroidx.compose.material3.TextComposablefunTodoMviScreen(vm:TodoMviViewModel){valuiStatebyvm.uiState.collectAsStateWithLifecycle()LaunchedEffect(Unit){vm.effect.collect{effect-when(effect){isTodoEffect.ShowToast-{println([模拟Toast]${effect.message})}}}}Column(modifierandroidx.compose.ui.Modifier.fillMaxSize()){Button(onClick{vm.sendIntent(TodoIntent.Refresh)}){Text(刷新列表)}when(uiState){isTodoUiState.Loading-CircularProgressIndicator()isTodoUiState.Success-{vallistData(uiStateasTodoUiState.Success).list LazyColumn{itemsIndexed(listData){index,item-Column{Text(textitem)Button(onClick{vm.sendIntent(TodoIntent.DeleteTodo(index))}){Text(删除)}}}}Button(onClick{vm.sendIntent(TodoIntent.AddTodo(新待办${System.currentTimeMillis()}))}){Text(➕新增待办)}}isTodoUiState.Error-{valerruiStateasTodoUiState.ErrorText(text❌${err.msg})}}}}逐行解读四层分层结构调用方向只能从上往下UI层Compose只负责渲染界面响应用户点击发送Intent给ViewModel不写业务逻辑不直接访问数据库、网络。ViewModel层接收Intent用户意图执行业务逻辑调用Repository生成新的UiState发出Effect一次性事件持有Flow向外暴露状态和事件。Repository仓库层协调本地数据源Room与远程网络Retrofit做数据合并、缓存策略ViewModel只和Repository打交道。数据源层Retrofit网络请求Room本地数据库纯粹的数据读写。调用方向UI → ViewModel → Repository → 数据源禁止下层反向持有上层引用数据库不可以回调Compose仓库不能直接调用UI。MVI三要素Intent用户意图把页面上所有用户操作点击刷新、新增、删除全部封装成密封类IntentUI层不直接调用ViewModel的零散业务函数统一走sendIntent(intent)好处便于后续调试、日志埋点。State页面状态UiState‑StateFlow页面所有常驻展示的数据加载、成功、错误驱动页面渲染。Effect一次性事件SharedFlowToast、弹窗、页面跳转瞬时消费不保留状态。完整单向数据流链路用户点击按钮(UI) → 构造Intent调用sendIntent()(ViewModel接收意图) → ViewModel调用Repository → Repository访问网络/本地数据库拿到结果原路返回数据ViewModel生成新UiState或者EffectFlow下发UI收集Flow页面重组刷新。Repository职责Repository里面可以实现缓存优先策略优先读取Room本地缓存同时后台发起网络请求回来之后更新本地数据库上层ViewModel感知到数据变更直接刷新UI。容易踩坑❌ UI层直接发起网络请求或者数据库读写业务散落在Compose组件后期难以维护。❌ Repository持有ViewModel或者Compose的引用下层反向回调上层极易造成内存泄漏。❌ 不用Intent封装用户动作ViewModel暴露大量零散业务函数后续埋点、测试不方便。✅ MVIUI发送IntentViewModel处理生成UiState常驻状态 / Effect瞬时事件单向数据流。多元化习题练习习题1【基础‑改错题】开发在Composable点击回调里面直接写Retrofit网络请求有什么问题答案Compose重组会反复执行网络逻辑写UI层页面重组可能重复发起请求同时业务逻辑散落在UI组件不利于单元测试网络请求应当放到RepositoryViewModel调用仓库完成请求。解读UI层只处理界面交互业务和数据交给下层。习题2【基础‑代码修改题】现在待办页面增加“清空全部待办”用户操作请在TodoIntent密封类新增对应的意图。答案objectClearAll:TodoIntent()解读无附加参数的用户动作直接定义object之后ViewModel的sendIntent当中补充when分支处理该意图。习题3【进阶‑补充代码题】简述MVI当中State与Effect的区别。答案State(UiState)页面常驻状态保存在StateFlow页面重组之后依然存在用于页面渲染展示Effect一次性瞬时事件toast、弹窗、跳转使用SharedFlow消费之后消失页面重建之后不应该重复触发。解读这一组概念和前面StateFlow / SharedFlow是对应的。习题4【综合‑逻辑应用题】Repository层里面同时存在网络数据源和Room本地数据源请简单描述常见缓存策略。答案优先读取Room本地数据立刻展示旧数据同时发起网络请求网络返回成功之后把新数据写入RoomRoom的Flow自动通知上层ViewModel拿到最新数据刷新页面。解读先展示缓存避免白屏后台更新本地页面自动刷新这是移动端很常用的策略。习题5【概念简答题】四层架构为什么禁止下层反向调用上层仓库不能直接操作UI答案下层Repository、数据源不应该感知页面、ViewModel的存在如果下层直接操作UI页面销毁之后下层还持有UI引用会造成严重内存泄漏同时分层被破坏后续单元测试难以编写下层只能返回数据结果由上层ViewModel根据返回的数据来决定如何更新页面状态。解读单向向下依赖是分层架构最重要的约束。本节课知识点总结Compose项目四层分层UI层(Compose) → ViewModel层 → Repository仓库层 → 数据源层(Room/Retrofit)只能从上往下依赖。MVI三要素Intent封装用户所有交互意图UiState常驻页面状态Effect一次性瞬时事件。完整单向数据流UI发送IntentViewModel调用Repository拿数据生成State/EffectFlow驱动UI页面重组刷新。Repository负责协调网络与本地缓存实现缓存优先策略下层禁止反向持有上层引用防止内存泄漏。UI层只负责渲染和转发用户意图不要在Compose里面写业务和网络逻辑。下一课预告进阶第9课Compose性能优化重组、remember、key、derivedStateOfLazy列表优化