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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 20:50:18