☰
【Jetpack Compose进阶学与练】第6课:处理网络异常
2026/10/10 9:04:58 网站建设 项目流程

前言

上一课模拟了网络请求;但是之前示例只区分“加载中”和“加载成功”。
真实业务还会遇到:网络报错、服务返回错误、数据为空。
如果分散多个Flow布尔变量(isLoading、isError、isEmpty)维护状态很容易不一致。
使用Kotlin密封类(sealed class)把页面整体UI状态统一封装,一套状态驱动页面;这是Android现代开发最佳实践。

知识点清单

  1. 为什么不要分散多个Boolean标记(isLoading、isError)
  2. sealed class密封类定义页面状态:Loading / Success / Error / Empty
  3. ViewModel发起网络请求,捕获异常,修改页面状态Flow
  4. Compose UI使用when分支分别渲染加载页、成功页、错误页、空页面
  5. 错误页面支持点击重试

完整示例源码

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_uiStateFlow=MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)valuiStateFlow:StateFlow<ArticleUiState>=_uiStateFlowfunloadData(){viewModelScope.launch{_uiStateFlow.value=ArticleUiState.Loadingtry{delay(2000)//模拟三种业务分支;这里可以换成真实retrofit网络调用valrandom=(0..2).random()when(random){0->{_uiStateFlow.value=ArticleUiState.Success("📄 返回文章正文内容,业务加载成功")}1->{_uiStateFlow.value=ArticleUiState.Empty}2->{throwException("网络连接超时,请检查网络")}}}catch(ex:Exception){_uiStateFlow.value=ArticleUiState.Error(ex.message?:"未知异常")}}}}@ComposablefunArticleSealedPage(){valvm:ArticleSealedViewModel=androidx.lifecycle.viewmodel.compose.viewModel()valuiStatebyvm.uiStateFlow.collectAsStateWithLifecycle()Column(modifier=Modifier.fillMaxSize(),verticalArrangement=Arrangement.Center,horizontalAlignment=androidx.compose.ui.Alignment.CenterHorizontally){when(uiState){isArticleUiState.Loading->{CircularProgressIndicator()Text(text="数据加载中……")}isArticleUiState.Success->{valsuccessState=uiStateasArticleUiState.SuccessText(text=successState.content)}isArticleUiState.Empty->{Text(text="📭暂无数据")Button(onClick={vm.loadData()}){Text("点击重新加载")}}isArticleUiState.Error->{valerrorState=uiStateasArticleUiState.ErrorText(text="❌加载失败:${errorState.errMsg}")Button(onClick={vm.loadData()}){Text("点击重试")}}}}}

逐行解读

  1. 为什么拒绝分散的布尔标记(isLoading,isError)
    如果同时存在isLoading=true、isError=true;就出现状态冲突。多个布尔变量之间无法互斥;很容易出现非法中间状态。
    密封类sealed class的各个子类是互斥;同一时刻只能是其中一种状态:加载中 / 成功 / 空 / 错误;从根源避免状态矛盾。
  2. 密封类 ArticleUiState
  • Loading 对象:正在请求网络,展示loading转圈
  • Success(data) 数据类:保存成功之后返回业务数据
  • Empty 对象:请求成功,但是后端返回列表为空
  • Error(msg) 数据类:保存异常错误信息
  1. ViewModel网络请求 + try‑catch捕获异常
    网络请求代码包裹在 try‑catch ;捕获IO、网络异常;捕获之后把状态设置为Error并且带上错误提示消息。

真实项目这里替换Retrofit网络调用;依然保留try‑catch。

  1. UI层 when分支渲染不同页面
    UI拿到UiState,直接when做分支判断;不同状态渲染完全不一样的界面:转圈、正文、空页面、错误重试页面。
    错误页面、空页面上的重试按钮直接调用vm.loadData()重新发起加载。
  2. 单向数据流要点
    ViewModel内部修改 _uiStateFlow.value 更新密封状态;Compose收集Flow,重组UI;UI层不允许修改UiState。

容易踩坑

  1. ❌ 使用多个独立布尔变量isLoading、isError;会产生互相冲突非法状态。
  2. ❌ 网络请求忘记try‑catch;一旦网络抛出异常直接App崩溃。
  3. ❌ 在UI层直接构造ArticleUiState;状态变更应当全部由ViewModel内部完成。
  4. ✅ sealed密封类保证同一时间只能处于其中一种UI状态;when分支编译器还会强制检查所有分支,漏写分支编译报错。

多元化习题(练习)

习题1【基础‑改错题】

业务里面同时维护 var isLoading:Boolean 和 var isError:Boolean ,会存在什么隐患?
答案:
有可能出现isLoading=true同时isError=true;出现互相矛盾的状态;页面不知道应该展示加载转圈还是错误提示;使用密封类把页面整体状态封装,保证互斥,同一时刻只能是其中一种状态。
解读:
多布尔标记适合简单开关;页面整体的加载‑成功‑错误‑空,优先密封类。

习题2【基础‑代码修改题】

现在需要新增一种状态: NoLogin 未登录状态;需要在密封类增加一条;写出新增的代码。
答案:

objectNoLogin:ArticleUiState()

解读:
属于无附加数据的状态,直接使用object单例对象;之后UI的when分支里面必须补充 is ArticleUiState.NoLogin 分支,否则编译报错;密封类的优势就是编译期检查全部分支。

习题3【进阶‑补充代码题】

假设真实Retrofit调用发生Http业务错误(后台返回业务错误码,不是网络IO异常),需要设置UiState为Error,errMsg赋值“服务器返回业务错误,请稍后重试”;写出ViewModel内部这一行状态赋值代码。
答案:

_uiStateFlow.value=ArticleUiState.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分支;并且各个状态之间天然互斥,避免出现非法的中间状态。
解读:
密封类做页面状态是Compose+ViewModel项目非常普遍的写法。

本节课知识点总结

  1. 不要分散多个布尔变量维护页面加载状态;容易产生冲突非法状态。
  2. 使用sealed class密封类封装页面整体UiState;Loading / Success / Empty / Error。
  3. ViewModel网络请求使用try‑catch捕获异常;异常时赋值Error状态;业务为空赋值Empty;成功赋值Success。
  4. UI层when分支,根据UiState渲染加载页、正文、空页面、错误重试页面;错误页面提供重试按钮。
  5. 初次页面加载:ViewModel init{}或者LaunchedEffect;禁止直接在Composable顶层发起网络请求。

下一课预告:进阶第7课:Room本地数据库基础,ViewModel结合Room做本地持久化

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询