乐趣区

关于android:深入探索-Paging-30-分页加载来自网络和数据库的数据-MAD-Skills

欢送回到 MAD Skills 系列之 Paging 3.0!在上一篇文章《获取数据并绑定到 UI | MAD Skills》中,咱们在 ViewModel 中集成了 Pager,并利用配合 PagingDataAdapter 向 UI 填充数据,咱们也增加了加载状态指示器,并在呈现谬误时从新加载。

这次,咱们把难度晋升一个品位。目前为止,咱们都是间接通过网络加载数据,而这样的操作只实用于现实环境。咱们有时候可能遇到网络连接迟缓,或者齐全断网的状况。同时,即便网络状况良好,咱们也不会心愿本人的利用成为数据黑洞——在导航到每个界面时都拉取数据是一种非常节约的行为。

解决这一问题的办法便是从 本地缓存 加载数据,并且只在必要的时候进行刷新。对缓存数据的更新必须先达到本地缓存,再流传至 ViewModel。这样一来,本地缓存便可成为惟一可信的数据源。对咱们来说非常不便的是 Paging 库在 Room 库一些小小的帮忙下曾经能够应答这种场景。上面就让咱们开始吧!点击这里 查看 Paging: 显示数据及其加载状态视频,理解更多详情。

应用 Room 创立 PagingSource

因为咱们将要分页的数据源会来自本地而不是间接依赖 API,那么咱们要做的第一件事便是更新 PagingSource。好消息是,咱们要做的工作很少。是因为我后面提到的 “ 来自 Room 的小小帮忙 ” 吗?事实上这里的帮忙远不止于一点: 只须要在 Room 的 DAO 中为 PagingSource 增加申明,便可通过 DAO 获取 PagingSource

@Dao
interface RepoDao {
    @Query(
        "SELECT * FROM repos WHERE" +
            "name LIKE :queryString"
    )
    fun reposByName(queryString: String): PagingSource<Int, Repo>
}

咱们当初能够在 GitHubRepository 中更新 Pager 的构造函数来应用新的 PagingSource 了:

fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
        
        …
        val pagingSourceFactory = {database.reposDao().reposByName(dbQuery) }

        @OptIn(ExperimentalPagingApi::class)
        return Pager(
           config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE,
                enablePlaceholders = false
            ),
            pagingSourceFactory = pagingSourceFactory,
            remoteMediator = …,
        ).flow
    }

RemoteMediator

目前为止一切顺利……不过咱们如同遗记了什么。本地的数据库要如何填充数据呢?来看看 RemoteMediator,当数据库中的数据加载结束时,它负责从网络加载更多数据。让咱们看看它是如何工作的。

理解 RemoteMediator 的关键在于意识到它是一个回调。RemoteMediator 的后果永远不会展现在 UI 上,因为它只是 Paging 用于告诉作为开发者的咱们: PagingSource 的数据曾经耗尽。更新数据库并告诉 Paging,这是咱们本人的工作。与 PagingSource 相似,RemoteMediator 有两个泛型参数: 查问参数类型和返回值类型。

@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(…) : RemoteMediator<Int, Repo>() {…}

让咱们来仔细观察下 RemoteMediator 中的形象办法。第一个办法是 initialize(),它是在所有加载开始前,RemoteMediator 调用的第一个办法,它的返回值为 InitializeActionInitializeAction 能够是 LAUNCH_INITIAL_REFRESH,也能够是 SKIP_INITIAL_REFRESH。前者示意在调用 load() 办法时携带的加载类型为 refresh,后者意味着只有在 UI 明确发动申请时才会应用 RemoteMediator 执行刷新操作。在咱们的用例中,因为仓库状态可能更新得颇为频繁,所以咱们返回 LAUNCH_INITIAL_REFRESH

  override suspend fun initialize(): InitializeAction {return InitializeAction.LAUNCH_INITIAL_REFRESH}

接下来咱们来看 load 办法。load 办法在 loadTypePagingState 所定义的边界处调用,加载类型能够是 refreshappendprepend。这一办法负责获取数据,将其长久化在磁盘上并告诉处理结果,其后果能够是 ErrorSuccess。如果后果是 Error,加载状态将会反映这一后果,并可能重试加载。如果加载胜利,须要告诉 Pager 是否能够加载更多数据。

override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {val page = when (loadType) {
            LoadType.REFRESH -> …
            LoadType.PREPEND -> …
            LoadType.APPEND -> …
        }

        val apiQuery = query + IN_QUALIFIER

        try {val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)

            val repos = apiResponse.items
            val endOfPaginationReached = repos.isEmpty()
            repoDatabase.withTransaction {
                …
                repoDatabase.reposDao().insertAll(repos)
            }
            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        } catch (exception: IOException) {return MediatorResult.Error(exception)
        } catch (exception: HttpException) {return MediatorResult.Error(exception)
        }
    }

因为 load 办法是一个有返回值的挂起函数,所以 UI 能够准确地反映加载实现的状态。在上一篇文章中,咱们简要介绍了 withLoadStateHeaderAndFooter 扩大函数,并理解了如何应用它来加载头部和底部。咱们能够察看到,该扩大函数的名字中蕴含了一个类型: LoadState。让咱们进一步理解这一类型。

LoadState、LoadStates 以及 CombinedLoadStates

因为分页是一系列异步事件,所以通过 UI 反映加载数据的以后状态非常重要。在分页操作中,Pager 的加载状态是通过 CombinedLoadStates 类型示意的。

顾名思义,这个类型是其余示意加载信息的类型的组合。这些类型包含:

LoadState 是一个残缺形容下列加载状态的密封类:

  • Loading
  • NotLoading
  • Error

LoadStates 是蕴含以下三种 LoadState 值的数据类:

  • append
  • prepend
  • refresh

通常来讲,prependappend 加载状态会用于响应额定的数据获取,而 refresh 加载状态则用来响应初始加载、刷新和重试。

因为 Pager 可能会从 PagingSource 或者 RemoteMediator 加载数据,所以 CombinedLoadStates 有两个 LoadState 字段。其中名为 source 的字段用于 PagingSource,而名为 mediator 的字段用于 RemoteMediator

不便起见,CombinedLoadStatesLoadStates 类似,同样含有 refreshappendprepend 字段,它们会基于 Paging 的配置和其余语义反映 RemoteMediatorPagingSourceLoadState。请务必查看相干文档以确定这些字段在不同场景下的行为。

应用这些信息更新咱们的 UI 就像从 PagingAdapter 裸露的 loadStateFlow 中获取数据一样简略。在咱们的利用中,咱们能够在第一次加载时应用这些信息显示一个加载指示器:

lifecycleScope.launch {
    repoAdapter.loadStateFlow.collect { loadState ->
        // 在刷新出错时显示重试头部,并且展现之前缓存的状态或者展现默认的 prepend 状态
        header.loadState = loadState.mediator
            ?.refresh
            ?.takeIf {it is LoadState.Error && repoAdapter.itemCount > 0}
            ?: loadState.prepend

        val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
        // 显示空列表
        emptyList.isVisible = isListEmpty
        // 无论数据来自本地数据库还是近程数据,仅在刷新胜利时显示列表。list.isVisible =  loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
        // 在初始加载或刷新时显示加载指示器
        progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
        // 如果初始加载或刷新失败,显示重试状态
        retryButton.isVisible = loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
    }
}

咱们开始从 Flow 收集数据,并在 Pager 尚未加载且现存列表为空时,应用 CombinedLoadStates.refresh 字段展现进度条。咱们之所以应用 refresh 字段,是因为咱们只心愿在第一次启动利用、或者明确触发了刷新时才展现大进度条。咱们还能够查看是否有加载状态出错并告诉用户。

回顾

在本文中,咱们实现了以下性能:

  • 应用数据库作为惟一可信数据源,并对数据进行分页;
  • 应用 RemoteMediator 填充基于 Room 的 PagingSource;
  • 应用来自 PagingAdapter 的 LoadStateFlow 更新带有进度条的 UI。

感谢您的浏览,下一篇文章将是 本系列 的最初一篇,敬请期待。

欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!

退出移动版