关键词汇
PagingSource :定义了如何围绕现有数据定义的现有边界获取和刷新数据。
文章
MAD Skills 系列之 Paging 3.0!-MAD Skills 系列之 Paging 第一集
全新系列视频:现代 Android 开发 (MAD) 技巧系列教程:在线查看
Introduction to MAD Skills
The set of modern technologis that take you from coding to distribution what we recomment to make you Android development experience better.
This includes Kotlin,because it saves you time by eliminating tedious boilerplate code.
Android Studio,because it's a greate IDE.
Also ,it keep introducing new modern features all the time that make coding more efficient.
Apis that are a subset of Jetpack specifically aimed at making Android development easier.
And app bundles ,because they automatically improve you app download size and app distribution process.
Tune into this series .We'll be posting so much content ,it'll drive you mad.
what's you MAD socre?
The latest technologies of Modern Android Development
Paging - MAD Skills
Tired of writing page after page of abstraction for displaying large data sets in your app?
How about just one ? Turn over a new leaf with Paging 3. In this series of video, you'll learn how to use a paging library in you app.
We'll start by seeing how to page from one source;the network ,how to display the and its loading states.
We'll then go a setp further and how we can page from multiple source; the network and database.
And also learn how to handle common operations,like retrying . We will continue by getting fancy,seeing how to work with transformations, separators ,adding custom headers and footers, and how to implement a use cse ,like search. Finally,we'll end with a live Q&A session to answer all the question you might have.
That's a whole bookshelf's worth. To keep you library up to date,make sure to subscribe to the Android Developer's YouTube channel.
And I'll see you on the flip side.
Introduction to Paging - MAD Skills
使用Github 仓库的web api
Showing a list of data to a user is one of the most common UI patterns. When working with large data sets, you can improve app performance and reduce memory usage by fetching and displaying data asynchronously iin chunks.
This also allows your app to support infinite data sets. And if you 're pulling down network data,you probable want to a local cache as well. This gets complicated fast. But Android Jetpack paging3.0 can help.
In this video,we'll be integrating the Paging library into the data layer of an existing app, a Github repository search app. While doing that you'll also learn about the basic components of paging.
The app in its current form follows the Jetpack architecture guidelines with a repository,responsible for fetching the data we want to display. A view model transforms the data from the repository into a form consumable by the UI.
The UI dispalys the transform data from the API.
You may have noticed ,however ,there's quite a bit of code required to transform the data from where it's fetched to where it's consumed in the UI.
We have to keep track of lots of manual ,mutable state across a few layers of the app.
And this is a fairly generic requirement that can be absracted out for re-use.
And This is exactly where Paging 3.0 comes in. The Jetpack Paging library provides a convenient way of pulling ordered data sets from you data layer into your business layer, While offering transfromations ,caching,and other niceties for easy consumption in your UI. Let's get started.
The first thing we need to do is difine how we actually fetch data. Currently,our app does this in repository using the GitHub service. In the context of the Paging library,this functionality is implemented with a type called the PagingSource. Specifically ,the PagingSource defines how to fetch and refresh data around existing boundaries defined by existing data.
Ok,Let's implement it.
// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
//codelabs上pag3练习 https://developer.android.com/codelabs/android-paging?index=..%2F..index#0
//android开发者-paging库 :https://developer.android.com/topic/libraries/architecture/paging/v3-overview
GithubService.kt
const val IN_QUALIFIER = "in:name,description"
/**
* Github API communication setup via Retrofit.
*/
interface GithubService {
/**
* Get repos ordered by stars.
*/
@GET("search/repositories?sort=stars")
suspend fun searchRepos(
@Query("q") query: String, //apiQuery
@Query("page") page: Int, //position
@Query("per_page") itemsPerPage: Int //loadSize
): RepoSearchResponse
companion object {//kotlin中的伴生对象(companion object)到底是个什么东西?https://www.jianshu.com/p/ddfed1df606c
private const val BASE_URL = "https://api.github.com/"
fun create(): GithubService {
val logger = HttpLoggingInterceptor()
logger.level = Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubService::class.java)
}
}
}
GitHubPagingSource.kt代码
//GitHub page API is 1 based: https://developer.github.com/v3/#pagination
//PagingSource 和 Map 类似,都需要定义两个泛型类型: 分页的 Key 的类型和加载的数据的类型。
private const val GITHUB_STARTING_PAGE_INDEX = 1
class GitHubPagingSource(
private val service:GithubService,
private val query:String
) :PagingSource<Int,Repo>(){
// 刷新 Key 用于在初始加载的数据失效后下一个 PagingSource 的加载。
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
// 我们需要获取与最新访问索引最接近页面的前一个 Key(如果上一个 Key 为空,则为下一个 Key)
// anchorPosition 即为最近访问的索引
return state.anchorPosition?. let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
//load() 方法正如其名,是由 Paging 库所调用的,用于异步加载要显示的数据的方法。这一方法会在初始加载或者响应用户滑动至边界时调用。load 方法会传入一个 LoadParams 对象,您可以通过它来确定如何触发 load 方法的调用。
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val repos = response.items
val nextKey = if (repos.isEmpty()) {
null
} else {
// 初始加载大小为 3 * NETWORK_PAGE_SIZE
// 要保证我们在第二次加载时不会去请求重复的项目。
position + (params.loadSize / NETWORK_PAGE_SIZE)
}
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
}
}
}
Since we'll pulling data from the Github API, and app name for this in the app is GitHubPagingSource. To build it ,we need to define the following --the type of paging key. In our case,the Github API uses 1-based index numbers for pages,so the type is an int.
The type of data loaded--in our case ,we're loading repo items,where the data I retrieved is coming from the Github service. Our data source will be specific to a certain query. So we need to make sure that we're also passing the query information to the Github service.
With the shell of the GitHubPagingSource defined,we can now implement the methods to enable us to load our data.
There are two main methods to provide implements for ,load and getRefreshKey. Let's start with the load method. The load function will be called by the Paging library to asynchronously fetch more data to be displayed as the user scrolls around. The load params object keeps information related to the load operation,including the following, the key of the first page to be loaded-- if this is the first time the load is called,params.key will be null. In this case ,you will have to difine the initial page key. Load size ,the requested number of times to laod. The load function returns the load result. It can either be a LoadResult.Page if the result was successful,or LoadResult.Error in case of an error. When constructing the load result page ,pass null for the next key or the previous key if the load cannot be loaded in the corresponding direction.
For example,in our case,we could consider that if the network response was successful but the list was empty,we do not have any more data to be loaded.
So in that case,the next key can be null.
Note that by default,the initial load size is three times the page size. This ensure that the first time the list is loaded ,the user will see enough items without triggering too many network requests, if the user does not scroll past what's loaded. This is also something to consider when computing the next key in the PagingSource implementation.
Next ,we need to implement getRefreshKey. The refresh key is used for subsequent refresh calls to PagingSource.load. The first goal is the initial load,which uses the initial key provided by the pager.
A refresh happens whenever the Paging library when to load new data to replace the current list, for example on a swipe to refresh on invalidation due to database updates ,configuration changes,process death, et cetera.
Typically ,subsequent refresh calls will want to restart loading data centered around paging state.anchorPosition,which represents the most recently accessed index.
With the PagingSource defined,we can now create a Pager. A Pager is a class that is responsible for incrementally pulling chunks of data from the PagingSource as requested by the UI.
We'll be create our Pager in our repository class as it has reference to types we need to create a Github Pager Source.
GithubRepository.kt
private const val NETWORK_PAGE_SIZE = 30
class GithubRepository(private val service: GithubService) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
Log.d("GithubRepository", "New query: $query")
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}
}
Creating the Pager is simple enough. The first thing we need is PagingConfig,which defines basic parameters that goven how the Pager will fetch data. There are quite a few configuration options here.
But for this particular case,we're concerned with just two --the pageSize,the number of items to be loaded at once from the PagingSource; and enablePlaceholders ,whether the pagingData returns null for items that have not been loaded yet .
We typically want the pageSize to be large enough, at least enough to fill the visible area of the screen, but preferably, two to three times that, so the Pager doesnet have to keep fetching data over and over again to show enough material on the screen as the user scrolls.
With the Pager constructed, we can now get data from it. The type produced from the Pager is the PagingData.
The Paging library offers multiple ways of consuming PagingData as a stream, including :
Kotlin Flow via Pager.flow;
LiveData via Pager.liveData ;
RxJava Flowable via Pager.flowable ;
RxJava Observable via Pager.observable.
Each emission of PagingData is distinct window into its backing PagingSource.
As a user scrolls throught the list,PagingData will keep fetching from the PagingSource to provide content.
Should the PagingSource be invalidated, a new PagingData will be admitted to make sure the items paginated throught are in sync with what is displayed in the UI.
It may help to think of PagingData as a stream of events from the PagingSource as an instance in time.
This stream of PagingData is what the basis layer can then opt to transform before presenting it to the UI.
That's it for this episode. So far,we've covered what a PagingSource is and implemented one; defined a PagingConfig and used it to createa Pager;
exposed a stream of PagingData from the Pager.
Stay tuned to see how to consume PagingData in the UI and populate our list of repos.