define
- A Google page-loading library for Android;
- Paging3 is so different from the previous version that you can learn it as a new library
- Before, it was easy to use ListView and RecyclerView for paging, so why paging3?
- It provides a set of very reasonable paging architecture, we only need to write business logic according to the architecture it provides, can easily achieve paging function;
- Related knowledge points: coroutine, Flow, MVVM, RecyclerView, DiffUtil
advantages
- Use memory to cache data;
- Built-in request for deduplication, more efficient display of data;
- RecyclerView automatically loads more
- Support for Kotlin’s coroutines and flows, as well as LiveData and RxJava2
- Built-in state handling: refresh, error, load, etc
The usage process is as follows:
Requirements:
- Display all android-related open source libraries on GitHub, in order of number of stars, and return 5 data per page;
1. Introduce dependencies
Paging :paging- run-time KTX :3.0.0-beta03' // Test testImplementation "Androidx. paging: Paging -common-ktx:3.0.0-beta03" // [optional] RxJava support implementation "Androidx. The paging: the paging - rxjava2 - KTX: 3.0.0 - beta03" / / retrofit library network request implementation 'com. Squareup. Retrofit2: retrofit: 2.9.0' Implementation 'com. Squareup. Retrofit2: converter - gson: 2.9.0' / / dropdown refresh implementation 'androidx. Swiperefreshlayout: swiperefreshlayout: 1.1.0'Copy the code
2. Create data model class RepoResponse
class RepoResponse {
@SerializedName("items") val items:List<Repo> = emptyList()
}
data class Repo(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String,
@SerializedName("stargazers_count") val starCount: String,
)
Copy the code
3. Define the network request interface ApiService
interface ApiService { @GET("search/repositories? sort=stars&q=Android") suspend fun searRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse companion object { private const val BASE_URL = "https://api.github.com/" fun create(): ApiService { return Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } } }Copy the code
4. Configure the data source
- A custom subclass inherits PagingSource, and then overrides the load() function to provide data for the current page number, which is where Paging3 is actually used
- The two generic parameters of PagingSource are page number type and data item type
class RepoPagingSource(private val apiService: ApiService) : PagingSource<Int, Repo>() { override fun getRefreshKey(state: PagingState<Int, Repo>): Int? { return null } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> { return try { val page = params.key ? : 1 val pageSize = params.loadSize val repoResponse = apiService.searRepos(page, pageSize) val repoItems = repoResponse.items val prevKey = if (page > 1) page - 1 else null val nextKey = if (repoItems.isNotEmpty()) page + 1 else null LoadResult.Page(repoItems, prevKey, nextKey) } catch (e: Exception) { LoadResult.Error(e) } } }Copy the code
5. Implement interface requests in ViewModel
- A parameter to PagingConfig, prefetchDistance, is used to indicate how many pieces of data are preloaded from the bottom. Setting 0 means that the data is loaded from the bottom. The default value is paging size. To make the user insensitive to loading, increase the prefetch threshold appropriately, such as 5 times the page size.
- CachedIn () is an extension of Flow and is used to cache data returned by a server within the viewModelScope scope. In case the phone rotates vertically or vertically causing an Activity to be created, Paging 3 can read data directly from the cache. Instead of having to re-initiate the network request.
Object Repository {private const val PAGE_SIZE = 5 private val gitHubService = ApiService.create() fun getPagingData(): Flow<PagingData<Repo>> {// prefetchDistance, a parameter of PagingConfig, is used to indicate how many pieces of data are preloaded from the bottom, and 0 is used to indicate that the data is loaded from the bottom. The default value is paging size. // To make users insensitive to loading, appropriately increase the prefetch threshold. Return Pager(config = PagingConfig(pageSize = PAGE_SIZE, prefetchDistance = PAGE_SIZE * 5), PagingSourceFactory = {RepoPagingSource(gitHubService)}).flow}} ViewModel() { fun getPagingData(): Flow<PagingData<Repo>> { return Repository.getPagingData().cachedIn(viewModelScope) } }Copy the code
6. Realize RecyclerView Adapter
- You must inherit the PagingDataAdapter
class RepoAdapter : PagingDataAdapter<Repo, repoadapter.viewholder >(COMPARATOR) {companion object {// Because Paging 3 uses DiffUtil to manage data changes, So this COMPARATOR is mandatory. Private val COMPARATOR = object: DiffUtil.ItemCallback<Repo>() { override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean { return oldItem == newItem } } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ val binding: LayoutRepoItemBinding? =DataBindingUtil.bind(itemView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.binding? .repo=getItem(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view=LayoutInflater.from(parent.context).inflate(R.layout.layout_repo_item,parent,false) return ViewHolder(view) } }Copy the code
7. Implementation of FooterAdapter
- To implement loading more, must inherit from LoadStateAdapter,
- Retry (): Use Kotlin’s higher-order function to register click events for the retry button
class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() { class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) { val binding=holder.binding as LayoutFooterItemBinding when (loadState) { is LoadState.Error -> { binding.progressBar.visibility = View.GONE binding.retryButton.visibility = View.VISIBLE binding.retryButton.text = "Load Failed, Tap Retry" binding.retryButton.setOnClickListener { retry() } } is LoadState.Loading -> { binding.progressBar.visibility = View.VISIBLE binding.retryButton.visibility = View.VISIBLE binding.retryButton.text = "Loading" } is LoadState.NotLoading -> { binding.progressBar.visibility = View.GONE binding.retryButton.visibility = View.GONE } } } override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder { val binding: LayoutFooterItemBinding = LayoutFooterItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ViewHolder(binding) } }Copy the code
8. Use it in an Activity
- Madapter.submitdata () is the core that triggers Paging 3; It receives a PagingData argument, which we can get by calling the collect() function of the Flow object returned from the ViewModel. Collect () is a bit like subscribe() in Rxjava. The news will keep flowing in here. But since the collect() function is a hang function that can only be called in the coroutine scope, the lifecyclescopes.launch () function is called to launch a coroutine.
- Load more: through mAdapter. WithLoadStateFooter implementation;
- Drop-down refresh: here down to refresh is used with SwipeRefreshLayout, call in its OnRefreshListener mAdapter. Refresh (), and in mAdapter. In dealing with the drop-down addLoadStateListener refresh the UI logic;
- Although there is a withLoadStateHeader, it is not used to refresh, but to load the previous page, which does not take effect until the current start page is >1
class Paging3Activity : AppCompatActivity() { private val viewModel by lazy { ViewModelProvider(this).get(Paging3ViewModel::class.java) } private val mAdapter:RepoAdapter = RepoAdapter() override fun onCreate(savedInstanceState: Bundle?) {super.oncreate (savedInstanceState) // Use val Binding in the Activity: ActivityPaging3Binding = DataBindingUtil.setContentView(this, R.l ayout. Activity_paging3) binding. LifecycleOwner = this / / dropdown refresh binding refreshlayout. SetOnRefreshListener { MAdapter. Refresh ()} binding. RecyclerView. LayoutManager = LinearLayoutManager (this) / / add a footer binding.recyclerView.adapter = mAdapter.withLoadStateFooter(FooterAdapter { mAdapter.retry() }) // binding.recyclerView.adapter = repoAdapter.withLoadStateHeaderAndFooter( // header = HeaderAdapter { repoAdapter.retry() }, // footer = FooterAdapter { repoAdapter.retry() } // ) lifecycleScope.launch { viewModel.getPagingData().collect { MAdapter. SubmitData (it)}} / / monitor loading state mAdapter addLoadStateListener {/ / such as processing drop-down refresh logic when (it. Refresh) {is LoadState.NotLoading -> { binding.recyclerView.visibility = View.VISIBLE binding.refreshlayout.isRefreshing = false } is LoadState.Loading -> { binding.refreshlayout.isRefreshing = true binding.recyclerView.visibility = View.VISIBLE } is LoadState.Error -> { val state = it.refresh as LoadState.Error binding.refreshlayout.isRefreshing = false Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT) .show() } } } } }Copy the code
9. RemoteMediator
The difference between RemoteMediator and PagingSource
- PagingSource: Implementation of a single data source and how to find data from that data source, recommended for loading limited data sets (local database), such as Room, data source changes are mapped directly to the UI;
- RemoteMediator: Load network paging data and update it to the database, but changes to the data source cannot be mapped directly to the UI.
- You can use RemoteMediator to load paging updates from the network into the database, and use PagingSource to find the data from the database and display it on the UI
The use of RemoteMediator
- Defining data sources
// Select * from the database where Room is stored; // Select * from the database where Room is stored @Entity Data class RepoEntity(@primaryKey Val ID: Int, @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "description") val description: String, @ColumnInfo(name = "star_count") val starCount: String, @ColumnInfo(name = "page") val page: Int , ) //2. Define the data access object RepoDao @ the Dao interface RepoDao {@ Insert (onConflict = OnConflictStrategy. REPLACE) suspend fun insert(pokemonList: List<RepoEntity>) @Query("SELECT * FROM RepoEntity") fun get(): PagingSource<Int, RepoEntity> @Query("DELETE FROM RepoEntity") suspend fun clear() @Delete fun delete(repo: RepoEntity) @Update fun update(repo: RepoEntity) } //3. Define database.database (entities = [RepoEntity::class], version = Constants.DB_VERSION) abstract class AppDatabase: RoomDatabase() { abstract fun repoDao(): RepoDao companion object { val instance = AppDatabaseHolder.db } private object AppDatabaseHolder { val db: AppDatabase = Room .databaseBuilder( AppHelper.mContext, AppDatabase::class.java, Constants.db_name).allowMainThreadQueries().build()}} //4. Interface Constants {/** * Database name */ String DB_NAME = "jetPackDemodatabase.db "; /** * database version */ int DB_VERSION = 1; }Copy the code
- Implement RemoteMediator
/ / 1. RemoteMediator is experimental API, all realize RemoteMediator class / / need to add @ OptIn (ExperimentalPagingApi: : class) annotations, / / use OptIn annotations, Android {kotlinOptions {freeCompilerArgs += [" -xopt-in = kotlin.requiresoptin "]}} //2. RemoteMediator and PagingSource need to override the load() method. But the different parameter @ OptIn (ExperimentalPagingApi: : class) class RepoMediator (val API: ApiService, val db: AppDatabase) : RemoteMediator<Int, RepoEntity>() { override suspend fun load( loadType: LoadType, state: PagingState<Int, RepoEntity> ): MediatorResult {val repoDao = db. RepoDao () val pageKey = the when (loadType) {/ / PagingDataAdapter first visit or call the refresh () Loadtype. PREPEND -> return mediatorresult. Success(endOfPaginationReached =) loadType. REFRESH -> null // LoadType.PREPEND -> return MediatorResult APPEND -> {val lastItem = state.lastitemorNULL () if (lastItem == null) {return Mediatorresult. Success(endOfPaginationReached = true)} lastitem. page}} AppHelper. MContext. IsConnectedNetwork () {return MediatorResult. Success (endOfPaginationReached = true)} / / request data paging network is val page = pageKey ? : 0 val pageSize = Repository.PAGE_SIZE val result = api.searRepos(page, pageSize).items val endOfPaginationReached = result.isEmpty() val items = result.map { RepoEntity( id = it.id, name = it.name, description = it.description, starCount = it.starCount, Db.withtransaction {if (loadType== loadType.refresh){repodao.clear ()} repodao.insert (items) } return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } }Copy the code
- Build Pager in Repository
object Repository { const val PAGE_SIZE = 5 private val gitHubService = ApiService.create() private val db = Appdatabase. instance private val pagingConfig = pagingConfig (pageSize = PAGE_SIZE, Placeholder = true, // How far off a placeholder will be, // Default pageSize prefetchDistance = PAGE_SIZE, // Initialize the load quantity, Defaults to pageSize * 3 initialLoadSize = PAGE_SIZE) @ OptIn (ExperimentalPagingApi: : class) fun getPagingData2 () : Flow<PagingData<Repo>> { return Pager( config = pagingConfig, remoteMediator = RepoMediator(gitHubService, db) ) { db.repoDao().get() }.flow.map { pagingData -> pagingData.map { RepoEntity2RepoMapper().map(it) } } } } class RepoEntity2RepoMapper : Mapper<RepoEntity, Repo> { override fun map(input: RepoEntity): Repo = Repo( id = input.id, name = input.name, description = input.description, starCount = input.starCount ) }Copy the code
- Get the data in the ViewModel
class Paging3ViewModel : ViewModel() {
fun getPagingData2(): LiveData<PagingData<Repo>> =
Repository.getPagingData2().cachedIn(viewModelScope).asLiveData()
}
Copy the code
- Register an observer in the Activity
viewModel.getPagingData2().observe(this, {
mAdapter.submitData(lifecycle, it)
})
Copy the code
- After finishing the work, run the code and find that if there is no network, the data in the database will be loaded. If there is a network, it will request data from the network to update the database and refresh the UI interface