多模块Jetpack Compose应用中如何正确处理HTTP错误码并避免模块依赖耦合
我太懂你现在的困扰了——本来想靠多模块架构把各个层的职责拆得明明白白,结果数据层直接用上了Retrofit的HttpException,等于把网络层的实现细节直接泄露到了数据层,完全没享受到模块解耦的好处,还要处理不同HTTP错误码给前端返回对应提示对吧?咱们一步步来解决这个问题,既解耦模块,又能精准处理错误码。
核心问题拆解
你现在的问题本质是模块边界被打破了:数据层不应该依赖网络层的具体异常类(HttpException),网络层的细节(比如Retrofit的实现、HTTP状态码)应该被网络层自己封装好,只给上层暴露干净的业务接口。同时,通用的错误类型、结果类型应该抽离到公共模块,避免各个模块互相依赖。
具体解决方案步骤
1. 抽离通用类型到基础模块
首先,把Result密封接口、DataError密封接口和它的Network枚举,放到一个独立的base(或者core)模块里——这个模块是所有业务模块的依赖,只放通用的、无业务逻辑的类型。这样网络层、数据层、UI层都能使用这些通用类型,不用互相依赖。
示例基础模块代码:
// base模块 - Result.kt sealed interface Result<out D, out E : Error> { data class Success<out D, out E : Error>(val data: D) : Result<D, E> data class Error<out D, out E : Error>(val error: E) : Result<D, E> } // base模块 - DataError.kt sealed interface DataError : Error { enum class Network : DataError { BAD_REQUEST, UNAUTHORIZED, CONFLICT, INTERNAL_SERVER_ERROR, NO_CONNECTION, UNKNOWN } }
2. 改造网络层,统一处理HTTP异常
网络层的核心职责是处理所有和网络相关的细节:发起请求、捕获网络异常、转换HTTP状态码为通用错误类型。我们可以给AuthApi做一个包装类(比如AuthNetworkSource),把所有网络请求的异常处理都放在这里,不让HttpException暴露到数据层。
首先在网络层新增这个包装类:
// 网络层 - AuthNetworkSource.kt class AuthNetworkSource @Inject constructor( private val authApi: AuthApi ) { // 通用网络请求异常处理工具 private suspend fun <T> safeApiCall(call: suspend () -> T): Result<T, DataError.Network> { return try { val data = call() Result.Success(data) } catch (e: HttpException) { val error = when (e.code()) { 400 -> DataError.Network.BAD_REQUEST 401 -> DataError.Network.UNAUTHORIZED 409 -> DataError.Network.CONFLICT 500 -> DataError.Network.INTERNAL_SERVER_ERROR else -> DataError.Network.UNKNOWN } Result.Error(error) } catch (e: IOException) { // 处理无网络、连接超时等情况 Result.Error(DataError.Network.NO_CONNECTION) } catch (e: Exception) { Result.Error(DataError.Network.UNKNOWN) } } // 登录请求:封装Retrofit调用,返回处理好的Result suspend fun login(request: LoginRequest): Result<TokenPair, DataError.Network> { return safeApiCall { authApi.login(request) } } // 同理处理注册请求(记得给AuthApi的register函数补返回值,比如Response<Void>或用户信息) suspend fun register(request: RegisterRequest): Result<Unit, DataError.Network> { return safeApiCall { authApi.register(request) } } }
然后在NetworkModule里让Dagger自动处理AuthNetworkSource的依赖(它已经用@Inject标注了构造函数),这样数据层可以依赖AuthNetworkSource,而不是直接依赖AuthApi。
3. 改造数据层,移除Retrofit依赖
现在数据层的AuthRepositoryImpl只需要依赖AuthNetworkSource,不用再处理HttpException,专注于业务逻辑(比如缓存Token、转换数据格式):
// 数据层 - AuthRepositoryImpl.kt class AuthRepositoryImpl @Inject constructor( private val authNetworkSource: AuthNetworkSource, private val tokenManager: TokenManager // 比如用DataStore缓存Token ) : AuthRepository { override suspend fun signIn(email: String, password: String): Result<TokenPair, DataError.Network> { val result = authNetworkSource.login(LoginRequest(email, password)) // 业务逻辑:如果登录成功,缓存Token if (result is Result.Success) { tokenManager.saveAccessToken(result.data.accessToken) tokenManager.saveRefreshToken(result.data.refreshToken) } return result } override suspend fun signUp(email: String, password: String): Result<Unit, DataError.Network> { return authNetworkSource.register(RegisterRequest(email, password)) } }
这样改造后,数据层完全和Retrofit的细节隔离了——它既不知道HttpException,也不知道Retrofit的存在,只关心拿到的Result类型,完美符合多模块解耦的要求。
4. UI层根据错误类型显示对应提示
最后到UI层,你可以直接根据Result.Error里的DataError.Network枚举值,显示不同的用户提示:
// UI层 - 示例:登录ViewModel或Composable函数 when (val result = viewModel.signInState.value) { is Result.Success -> { // 登录成功,导航到主页 navController.navigate(Screen.Home.route) } is Result.Error -> { val message = when (result.error) { DataError.Network.BAD_REQUEST -> "请求参数错误,请检查输入格式" DataError.Network.UNAUTHORIZED -> "邮箱或密码错误,请重新输入" DataError.Network.CONFLICT -> "该邮箱已被注册,请直接登录" DataError.Network.INTERNAL_SERVER_ERROR -> "服务器内部错误,请稍后再试" DataError.Network.NO_CONNECTION -> "网络连接失败,请检查网络设置" DataError.Network.UNKNOWN -> "未知错误,请稍后再试" } // 显示Snackbar或Toast SnackbarHostState.showSnackbar(message) } }
为什么这样做是对的?
- 单一职责原则:网络层管网络请求和异常,数据层管业务逻辑,UI层管用户展示,每个模块只做自己的事。
- 模块解耦:数据层不再依赖网络层的具体实现,哪怕以后把Retrofit换成OkHttp或者其他网络库,数据层和UI层都不用改代码。
- 可维护性:所有网络异常的处理逻辑都集中在网络层的
safeApiCall函数里,不用每个请求都重复写try-catch,改错误码映射只需要改一个地方。
内容来源于stack exchange

