Skip to content

Repository

rongc edited this page Jul 25, 2021 · 5 revisions

NetworkBoundResource

一、存取方案

引用官方的解释

它首先观察资源的数据库。首次从数据库中加载条目时,NetworkBoundResource 会检查结果是好到足以分派,还是应从网络中重新获取。请注意,考虑到您可能会希望在通过网络更新数据的同时显示缓存的数据,这两种情况可能会同时发生。 如果网络调用成功完成,它会将响应保存到数据库中并重新初始化数据流。如果网络请求失败,NetworkBoundResource 会直接分派失败消息。 注意:在将新数据保存到磁盘后,我们会重新初始化来自数据库的数据流。不过,通常我们不需要这样做,因为数据库本身正好会分派更改。 请注意,依赖于数据库来分派更改将产生相关副作用,这样不太好,原因是,如果由于数据未更改而使得数据库最终未分派更改,就会出现这些副作用的未定义行为。 此外,不要分派来自网络的结果,因为这样将违背单一可信来源原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有新数据的情况下分派 SUCCESS,因为如果这样做,客户端会接收错误版本的数据。

看不下去对吧,大致整理下:

  1. 在发起请求前NetworkBoundResource先检查数据库,然后判断是否直接使用还是继续请求网络数据。
  2. 可在网络请求期间先使用缓存数据。
  3. 如果网络请求成功会更新数据库并向外派送通知,如果网络请求失败会直接派送失败消息。
  4. 不要依赖数据库的变动通知虽然他有这个功能,因为可能数据库未更改而使得这个通知未派送。
  5. 不要直接分派来自网络请求的结果,因为违背了单一可信赖原则。在保存期间可能需要更改数据。

决策树

abstract class NetworkBoundResource<ResultType, RequestType> {
    private val result = MediatorLiveData<Resource<ResultType>>()
        
    protected open fun onFetchFailed() {}
    
    @WorkerThread
    protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body

    @WorkerThread
    protected abstract fun saveCallResult(item: RequestType)

    @MainThread
    protected abstract fun shouldFetch(data: ResultType?): Boolean

    @MainThread
    protected abstract fun loadFromDb(): LiveData<ResultType>

    @MainThread
    protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

NetworkBoundResource定义了两个类型参数(ResultType 和 RequestType),因为从 API 返回的数据类型可能与本地使用的数据类型并不相同。 RequestType为Api返回的类型,ResultType为本地需要的类型。

简要分析下整个工作流程:

  1. 当程序发起请求时会将状态更新为loading。
  2. 通过loadFromDb()从数据库中加载数据,并通过shouldFetch(data)询问是否需要继续从网络获取。
  3. 如果不需要从网络获取则派发这个结果,否则使用createCall()创建网络请求。
  4. 当请求完成时:
    • 请求成功(ApiSuccessResponse):执行processResponse(),可在此时加工数据。然后调用saveCallResult() 将结果保存到数据库。之后重新loadFromDb()才把结果派送出去,Resource状态为Success。
    • 请求为空(ApiEmptyResponse):重新loadFromDb()把结果派送出去,Resource状态为Success。
    • 请求失败(ApiErrorResponse):调用onFetchFailed(),并把上次从数据库取到的数据和此错误派送出去,Resource状态为error。

二、数据仓库

先看下官方推荐的架构图

architecture

可以看到,除了存储区(Repository),其他组件都仅依赖下一级各司其职。 存储区依赖可以是本地数据库或者远程,相比无论数据是否有更新每次都从远程获取数据,使用本地数据或者在从远程获取最新数据期间使用本地缓存数据以缩减用户等待时间,然后在数据请求回来后更新到数据库以供下次使用,这无疑是更友好的做法。

无论Repository决定从使用哪个数据源,它的职责都是提供单一可信来源的数据。

三、使用

  1. 创建数据仓库, 提供远程和本地数据源;
  2. 添加数据获取方法,根据情况选择数据源;
class RepoRepository(val repoApi: RepoService, val repoDb: GithubDb) {

    // 发起同一条件的查询时,距离上次请求时长超过$timeout后将重新发起网络请求
    private val repoListRateLimit = RateLimiter<String>(10, TimeUnit.SECONDS)

    fun searchRepo(query: String, page: Int): LiveData<Resource<List<Repo>>> {
        return object : NetworkBoundResource<List<Repo>, RepoSearchResponse>() {
            // 加载缓存
            override fun loadFromDb(): LiveData<List<Repo>> {
                return repoDao.search(query).switchMap { searchData ->
                    if (searchData == null) {
                        AbsentLiveData.create()
                    } else {
                        repoDao.loadOrdered(searchData.repoIds)
                    }
                }
            }

            // 根据缓存数据判断是否要重新发起网络请求
            override fun shouldFetch(data: List<Repo>?): Boolean {
                // demo未实现数据库分页加载
                return (data == null || repoListRateLimit.shouldFetch(query))
            }

            // 发起网络请求
            override fun createCall(): LiveData<ApiResponse<RepoSearchResponse>> {
                return repoApi.searchRepos(query, page)
            }

            // 请求结果缓存(saveCallResult)前可再次加工
            override fun processResponse(response: ApiSuccessResponse<RepoSearchResponse>): RepoSearchResponse {
                return super.processResponse(response)
            }

            // 缓存请求到的数据
            override fun saveCallResult(item: RepoSearchResponse) {
                val repoIds = item.items.map { it.id }
                val repoSearchResult = RepoSearchResult(
                    query = query,
                    repoIds = repoIds,
                    totalCount = item.total,
                    next = item.nextPage
                )
                repoDb.runInTransaction {
                    repoDb.repoDao().insertRepos(item.items)
                    repoDb.repoDao().insert(repoSearchResult)
                }
            }

            // 请求失败
            override fun onFetchFailed() {
                repoListRateLimit.reset(query)
            }
        }.asLiveData()
    }
}
  1. 另外,如果某些请求不需要缓存数据可以使用networkOnly()方法。
    fun getRepos(owner: String): LiveData<Resource<List<Repo>>> {
        // 只发起网络请求,不做缓存
        return repoApi.getRepos(owner).networkOnly()
    }

ViewModel中:

class RepoSearchViewModel(private val repository: RepoRepository) : BaseViewModel() {

    fun getRepos(owner: String?): LiveData<Resource<List<Repo>>> {
        return if (owner.isNullOrEmpty()) {
            AbsentLiveData.create()
        } else {
            repository.getRepos(owner)
        }
    }
}

View中订阅:

class RepoSearchFragment : BaseFragment<FragmentListBinding, RepoSearchViewModel>() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         viewModel.getRepos().observe(viewLifecycleOwner) {
            showProgressIfLoading(it)
            when (it.status) {
               Status.LOADING -> {}
               Status.SUCCESS -> {}
               Status.ERROR -> {}
            }
         }
    }
}

注意:ApiService中定义的返回值类型为ApiResponse,是为了包含不同的请求结果。而订阅时为了及时观察到请求的状态,返回值被转化为Resource。更多请看Status

另:自3.0.2起,支持通过协程方式发起请求

class RepoViewModel : BaseViewModel() {
    fun fetchData(): LiveData<Resource<String>> {
        return launch {
            repository.fetchData()
        }
    }
}

class XXFragment : BaseFragment() {
    override fun onViewCreate(...) {
        viewModel.fetchData().observe(this) { 
            if (it.isSuccess) {
                ...
            }
        }
    }
}

Clone this wiki locally