ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Jetpack Compose进阶学与练】第6课:处理网络异常

【Jetpack Compose进阶学与练】第6课:处理网络异常 前言上一课模拟了网络请求但是之前示例只区分“加载中”和“加载成功”。真实业务还会遇到网络报错、服务返回错误、数据为空。如果分散多个Flow布尔变量isLoading、isError、isEmpty维护状态很容易不一致。使用Kotlin密封类(sealed class)把页面整体UI状态统一封装一套状态驱动页面这是Android现代开发最佳实践。知识点清单为什么不要分散多个Boolean标记isLoading、isErrorsealed class密封类定义页面状态Loading / Success / Error / EmptyViewModel发起网络请求捕获异常修改页面状态FlowCompose UI使用when分支分别渲染加载页、成功页、错误页、空页面错误页面支持点击重试完整示例源码importandroidx.lifecycle.ViewModelimportandroidx.lifecycle.viewModelScopeimportkotlinx.coroutines.delayimportkotlinx.coroutines.flow.MutableStateFlowimportkotlinx.coroutines.flow.StateFlowimportkotlinx.coroutines.launchimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.collectAsStateWithLifecycleimportandroidx.compose.runtime.getValueimportandroidx.compose.ui.Modifierimportandroidx.compose.foundation.layout.Arrangementimportandroidx.compose.foundation.layout.Columnimportandroidx.compose.foundation.layout.fillMaxSizeimportandroidx.compose.material3.Buttonimportandroidx.compose.material3.CircularProgressIndicatorimportandroidx.compose.material3.Text//密封类统一页面UI状态sealedclassArticleUiState{objectLoading:ArticleUiState()dataclassSuccess(valcontent:String):ArticleUiState()dataclassError(valerrMsg:String):ArticleUiState()objectEmpty:ArticleUiState()}classArticleSealedViewModel:ViewModel(){privateval_uiStateFlowMutableStateFlowArticleUiState(ArticleUiState.Loading)valuiStateFlow:StateFlowArticleUiState_uiStateFlowfunloadData(){viewModelScope.launch{_uiStateFlow.valueArticleUiState.Loadingtry{delay(2000)//模拟三种业务分支这里可以换成真实retrofit网络调用valrandom(0..2).random()when(random){0-{_uiStateFlow.valueArticleUiState.Success( 返回文章正文内容业务加载成功)}1-{_uiStateFlow.valueArticleUiState.Empty}2-{throwException(网络连接超时请检查网络)}}}catch(ex:Exception){_uiStateFlow.valueArticleUiState.Error(ex.message?:未知异常)}}}}ComposablefunArticleSealedPage(){valvm:ArticleSealedViewModelandroidx.lifecycle.viewmodel.compose.viewModel()valuiStatebyvm.uiStateFlow.collectAsStateWithLifecycle()Column(modifierModifier.fillMaxSize(),verticalArrangementArrangement.Center,horizontalAlignmentandroidx.compose.ui.Alignment.CenterHorizontally){when(uiState){isArticleUiState.Loading-{CircularProgressIndicator()Text(text数据加载中……)}isArticleUiState.Success-{valsuccessStateuiStateasArticleUiState.SuccessText(textsuccessState.content)}isArticleUiState.Empty-{Text(text暂无数据)Button(onClick{vm.loadData()}){Text(点击重新加载)}}isArticleUiState.Error-{valerrorStateuiStateasArticleUiState.ErrorText(text❌加载失败${errorState.errMsg})Button(onClick{vm.loadData()}){Text(点击重试)}}}}}逐行解读为什么拒绝分散的布尔标记isLoadingisError如果同时存在isLoadingtrue、isErrortrue就出现状态冲突。多个布尔变量之间无法互斥很容易出现非法中间状态。密封类sealed class的各个子类是互斥同一时刻只能是其中一种状态加载中 / 成功 / 空 / 错误从根源避免状态矛盾。密封类 ArticleUiStateLoading 对象正在请求网络展示loading转圈Success(data) 数据类保存成功之后返回业务数据Empty 对象请求成功但是后端返回列表为空Error(msg) 数据类保存异常错误信息ViewModel网络请求 try‑catch捕获异常网络请求代码包裹在 try‑catch 捕获IO、网络异常捕获之后把状态设置为Error并且带上错误提示消息。真实项目这里替换Retrofit网络调用依然保留try‑catch。UI层 when分支渲染不同页面UI拿到UiState直接when做分支判断不同状态渲染完全不一样的界面转圈、正文、空页面、错误重试页面。错误页面、空页面上的重试按钮直接调用vm.loadData()重新发起加载。单向数据流要点ViewModel内部修改 _uiStateFlow.value 更新密封状态Compose收集Flow重组UIUI层不允许修改UiState。容易踩坑❌ 使用多个独立布尔变量isLoading、isError会产生互相冲突非法状态。❌ 网络请求忘记try‑catch一旦网络抛出异常直接App崩溃。❌ 在UI层直接构造ArticleUiState状态变更应当全部由ViewModel内部完成。✅ sealed密封类保证同一时间只能处于其中一种UI状态when分支编译器还会强制检查所有分支漏写分支编译报错。多元化习题练习习题1【基础‑改错题】业务里面同时维护 var isLoading:Boolean 和 var isError:Boolean 会存在什么隐患答案有可能出现isLoadingtrue同时isErrortrue出现互相矛盾的状态页面不知道应该展示加载转圈还是错误提示使用密封类把页面整体状态封装保证互斥同一时刻只能是其中一种状态。解读多布尔标记适合简单开关页面整体的加载‑成功‑错误‑空优先密封类。习题2【基础‑代码修改题】现在需要新增一种状态 NoLogin 未登录状态需要在密封类增加一条写出新增的代码。答案objectNoLogin:ArticleUiState()解读属于无附加数据的状态直接使用object单例对象之后UI的when分支里面必须补充 is ArticleUiState.NoLogin 分支否则编译报错密封类的优势就是编译期检查全部分支。习题3【进阶‑补充代码题】假设真实Retrofit调用发生Http业务错误后台返回业务错误码不是网络IO异常需要设置UiState为ErrorerrMsg赋值“服务器返回业务错误请稍后重试”写出ViewModel内部这一行状态赋值代码。答案_uiStateFlow.valueArticleUiState.Error(服务器返回业务错误请稍后重试)解读不光try‑catch捕获的异常业务逻辑判断发现后台业务错误同样修改UiStateFlow流转到错误UI分支。习题4【综合‑逻辑应用题】页面进入的时候自动发起网络加载请问数据加载的调用写在哪里写在Composable直接调用vm.loadData()可以吗为什么答案不可以直接在Composable顶层调用vm.loadData()页面重组的时候会反复触发加载应当使用LaunchedEffect(Unit){ vm.loadData() }仅页面首次出现执行一次也可以直接在ViewModel的init块里面发起初次加载。解读init块在ViewModel创建的时候执行一次LaunchedEffect(Unit)在页面首次渲染执行一次二选一禁止直接写在Composable顶层。习题5【概念简答题】密封类用于页面UiState相比普通class有什么好处答案密封类的子类集合是固定封闭when分支时Kotlin编译器强制检查所有子类漏写分支直接编译报错防止漏掉UI分支并且各个状态之间天然互斥避免出现非法的中间状态。解读密封类做页面状态是ComposeViewModel项目非常普遍的写法。本节课知识点总结不要分散多个布尔变量维护页面加载状态容易产生冲突非法状态。使用sealed class密封类封装页面整体UiStateLoading / Success / Empty / Error。ViewModel网络请求使用try‑catch捕获异常异常时赋值Error状态业务为空赋值Empty成功赋值Success。UI层when分支根据UiState渲染加载页、正文、空页面、错误重试页面错误页面提供重试按钮。初次页面加载ViewModel init{}或者LaunchedEffect禁止直接在Composable顶层发起网络请求。下一课预告进阶第7课Room本地数据库基础ViewModel结合Room做本地持久化
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进