Android网络请求Result包装器结合缓存实现的相关疑问
Android仓库层结合Room缓存与Resource密封类的实践方案
现有实现回顾
在Android开发中,仓库层执行网络请求后常以Resource密封类包装结果返回,原有实现代码如下:
原Resource密封类
sealed class Resource<T> { class Success<T>(val data: T) : Resource<T>() class Error<T>(val error: Throwable? = null) : Resource<T>() }
原仓库层调用示例
suspend fun getUsers(): Resource<List<User>> { return try { // response is List<User> val response = withContext(Dispatchers.IO) { service.getAllUsers() } Resource.Success(response) } catch (e: Throwable) { Timber.d(e.message) Resource.Error(e) } }
针对结合Room数据库缓存的场景,以下是对应疑问的解决方案:
1. 网络错误时返回缓存并让ViewModel感知异常
要实现这个需求,需要调整Resource的结构,让成功状态可以携带异常信息(标记这是缓存数据且网络请求失败),具体实现如下:
调整后的Resource密封类
sealed class Resource<T> { // 成功状态:可携带网络异常(用于缓存场景) data class Success<T>(val data: T, val networkError: Throwable? = null) : Resource<T>() // 错误状态:无缓存数据时返回 data class Error<T>(val error: Throwable) : Resource<T>() }
结合Room的仓库层实现
suspend fun getUsers(): Resource<List<User>> { return withContext(Dispatchers.IO) { // 先查询本地缓存 val cachedUsers = userDao.getUsers() return@withContext try { // 发起网络请求 val networkUsers = service.getAllUsers() // 更新本地缓存 userDao.insertAll(networkUsers) // 返回最新网络数据,无异常标记 Resource.Success(networkUsers) } catch (e: Throwable) { Timber.d(e.message) if (cachedUsers.isNotEmpty()) { // 网络错误但有缓存:返回缓存数据,并携带异常让ViewModel感知 Resource.Success(cachedUsers, e) } else { // 无缓存且网络错误:返回纯错误状态 Resource.Error(e) } } } }
ViewModel中的处理逻辑
viewModelScope.launch { when(val result = repository.getUsers()) { is Resource.Success -> { // 更新UI显示数据 _users.value = result.data // 检查是否有网络异常,存在则提示用户 result.networkError?.let { showNetworkErrorTip(it.message) } } is Resource.Error -> { // 无缓存且网络错误,显示错误页面 showErrorPage(result.error.message) } } }
2. 是否需要分离数据获取与存储逻辑
建议分离,遵循单一职责原则,让仓库层代码更清晰、易维护:
- 将网络请求、数据库查询等数据获取逻辑单独封装
- 将数据库写入、缓存更新等数据存储逻辑单独封装
分离后的仓库层示例
// 数据获取逻辑 private suspend fun fetchUsersFromNetwork(): List<User> { return service.getAllUsers() } private suspend fun fetchUsersFromCache(): List<User> { return userDao.getUsers() } // 数据存储逻辑 private suspend fun saveUsersToCache(users: List<User>) { userDao.insertAll(users) } // 对外暴露的业务方法 suspend fun getUsers(): Resource<List<User>> { return withContext(Dispatchers.IO) { val cachedUsers = fetchUsersFromCache() return@withContext try { val networkUsers = fetchUsersFromNetwork() saveUsersToCache(networkUsers) Resource.Success(networkUsers) } catch (e: Throwable) { Timber.d(e.message) if (cachedUsers.isNotEmpty()) { Resource.Success(cachedUsers, e) } else { Resource.Error(e) } } } }
这样做的优势:
- 代码模块化,便于单元测试(比如单独测试缓存存储逻辑)
- 后续更换缓存方案(比如从Room换成MMKV)时,只需修改存储相关方法,不影响业务逻辑
- 降低代码耦合度,可读性更强
3. 是否应弃用Result包装器?适用场景与弊端
不建议弃用,但需要根据场景合理设计包装器的结构:
适用场景
- 跨层状态统一传递:仓库层将网络、数据库操作的结果(成功、错误、缓存)统一封装,让ViewModel无需处理底层异常细节
- 状态明确区分:避免上层出现大量
try-catch,通过密封类的分支处理不同状态,代码更严谨 - 复杂业务场景:比如需要同时返回数据和状态信息(如缓存标记、异常原因)时,包装器能清晰承载这些信息
弊端
- 过度封装冗余:如果是简单的本地数据库查询(无异常风险),使用包装器会增加不必要的代码复杂度
- 设计不合理导致信息缺失:原有的
Resource仅包含Success和Error,无法区分"网络成功"和"缓存成功",需要额外扩展 - 泛型使用成本:如果泛型处理不当,可能会出现空值安全问题,需要额外的空判断
内容的提问来源于stack exchange,提问作者tscheppe
相关产品推荐
相关产品推荐

