如何在Fragment中处理Retrofit超时,且避免传递UI组件?
最优解决方案:通过事件流分离UI与ViewModel逻辑
你的问题核心是ViewModel中的异步异常无法被Fragment的同步catch块捕获——因为ViewModel里的Retrofit网络请求是在协程/异步线程中执行的,Fragment点击事件里的catch只能捕获当前线程的同步异常,异步任务的异常会被协程内部处理,不会冒泡到外层调用方。
直接把UI组件传给ViewModel的做法完全不推荐:这违反了MVVM的职责分离原则,ViewModel不该依赖任何UI层类,会导致内存泄漏(ViewModel生命周期远长于Fragment)、ViewModel无法复用、单元测试困难等问题。
下面是两种符合最佳实践的方案,优先推荐第一种:
方案一:用StateFlow传递错误事件(Jetpack官方推荐)
通过ViewModel定义事件流,将错误类型传递给Fragment,由Fragment负责UI更新,彻底分离业务逻辑与UI操作。
步骤1:定义错误类型密封类
用来区分不同错误场景,方便Fragment做针对性处理:
sealed class LoginError { object Timeout : LoginError() // 超时错误 object NetworkFailed : LoginError() // 其他网络错误 object InvalidCredentials : LoginError() // 账号密码错误等业务错误 }
步骤2:ViewModel中添加事件流并处理异常
class LoginViewModel : ViewModel() { // 内部可修改的错误事件流 private val _loginError = MutableStateFlow<LoginError?>(null) // 对外暴露只读流,供Fragment监听 val loginError: StateFlow<LoginError?> = _loginError // 控制进度条显示的状态流 private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow<Boolean> = _isLoading fun login(username: String, password: String) = viewModelScope.launch { _isLoading.value = true try { // 调用Retrofit登录接口 val response = apiService.login(username, password) // 处理登录成功逻辑(比如跳转主页) } catch (e: IOException) { // Retrofit超时属于IOException,发送超时事件 _loginError.value = LoginError.Timeout } catch (e: HttpException) { // 处理HTTP错误(如401账号密码错误) _loginError.value = LoginError.InvalidCredentials } catch (e: Exception) { // 其他异常 _loginError.value = LoginError.NetworkFailed } finally { _isLoading.value = false } } // 重置错误事件,避免重复触发UI更新 fun clearLoginError() { _loginError.value = null } }
步骤3:Fragment中监听事件并更新UI
在Fragment的onViewCreated方法中,通过生命周期感知的协程监听ViewModel的事件流,根据事件类型更新UI:
class LoginFragment : Fragment() { private val viewModel: LoginViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 监听加载状态,控制进度条显示 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.isLoading.collect { isLoading -> loginProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE } } } // 监听错误事件,更新错误提示 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.loginError.collect { error -> error?.let { when(it) { LoginError.Timeout -> { loginTimeoutErrorTextview.visibility = View.VISIBLE // 隐藏其他错误提示 } LoginError.InvalidCredentials -> { // 显示账号密码错误提示 } LoginError.NetworkFailed -> { // 显示通用网络错误提示 } } // 消费事件后重置,避免屏幕旋转等场景重复触发 viewModel.clearLoginError() } } } } // 登录按钮点击事件 loginButton.setOnClickListener { val username = usernameEditText.text.toString() val password = passwordEditText.text.toString() // 隐藏之前的错误提示 loginTimeoutErrorTextview.visibility = View.GONE // 调用ViewModel的登录方法 viewModel.login(username, password) } } }
方案二:用回调接口(不推荐,仅作补充)
如果不想用Flow,也可以通过回调接口传递错误,但需要用弱引用避免内存泄漏:
步骤1:定义回调接口
interface LoginErrorCallback { fun onTimeoutError() fun onOtherError() }
步骤2:ViewModel中持有回调弱引用
class LoginViewModel : ViewModel() { private var errorCallback: WeakReference<LoginErrorCallback>? = null fun setErrorCallback(callback: LoginErrorCallback) { errorCallback = WeakReference(callback) } fun login(username: String, password: String) = viewModelScope.launch { try { // 调用Retrofit接口 } catch (e: IOException) { errorCallback?.get()?.onTimeoutError() } catch (e: Exception) { errorCallback?.get()?.onOtherError() } } }
步骤3:Fragment实现回调并绑定
class LoginFragment : Fragment(), LoginErrorCallback { private val viewModel: LoginViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.setErrorCallback(this) } override fun onTimeoutError() { loginProgressBar.visibility = View.GONE loginTimeoutErrorTextview.visibility = View.VISIBLE } override fun onOtherError() { // 处理其他错误 } override fun onDestroyView() { super.onDestroyView() // 清空回调,避免内存泄漏 viewModel.setErrorCallback(null) } }
这个方案需要手动管理回调生命周期,不如Flow的生命周期感知特性优雅,所以优先推荐方案一。
内容的提问来源于stack exchange,提问作者EkorrTi
相关产品推荐
相关产品推荐

