You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 03:24:57