前面我們使用Java來(lái)運(yùn)用JetPack中的一系列組件胜蛉,又使用kotlin運(yùn)用這些組件實(shí)現(xiàn)了一系列功能:
- kotlin--Flow文件下載
- kotlin--Flow結(jié)合Room運(yùn)用
- kotlin--Flow結(jié)合retrofit運(yùn)用
- kotlin--StateFlow運(yùn)用
- kotlin--SharedFlow運(yùn)用
接著,Jetpack的Paging3中色乾,我們使用的語(yǔ)言是kotlin誊册,相信通過(guò)這些項(xiàng)目的對(duì)比,你就能發(fā)現(xiàn)koltin取代Java的理由了暖璧,kotlin擁有更好的擴(kuò)展性案怯,更高的性能,更簡(jiǎn)潔的代碼澎办,更好的Jetpack組件支持嘲碱,如果你還對(duì)kotlin不熟悉,那么可以查閱我的kotlin專題博客局蚀,在此也要感謝動(dòng)腦學(xué)院Jason老師的辛勤付出麦锯,動(dòng)腦學(xué)院在B站上也有投稿koltin基礎(chǔ)的視頻,通過(guò)視頻可以快速學(xué)習(xí)和上手kotlin
今天來(lái)綜合使用各種組件琅绅,搭建最新MVVM項(xiàng)目框架扶欣,利用Paging3實(shí)現(xiàn)列表功能,Paging3和Paging2一樣,支持?jǐn)?shù)據(jù)庫(kù)緩存
一宵蛀、依賴
主項(xiàng)目gradle中導(dǎo)入hilt插件
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28.1-alpha'
}
module依賴hilt昆著、kapt插件
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
DataBinding、ViewBinding支持:
buildFeatures {
dataBinding = true
viewBinding = true
}
在kotlin1.5.20下使用Hilt編譯會(huì)出現(xiàn)問(wèn)題:
Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?
解決方法:
kapt {
javacOptions {
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
}
}
依賴各大組件:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
implementation "io.coil-kt:coil:1.1.0"
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation "androidx.startup:startup-runtime:1.0.0"
def hilt_version = "2.28-alpha"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
def hilt_view_version = "1.0.0-alpha01"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_view_version"
kapt "androidx.hilt:hilt-compiler:$hilt_view_version"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-beta03'
二术陶、Hilt注入
Hilt注解釋義:
- @HiltAndroidApp:觸發(fā)Hilt的代碼生成
- @AndroidEntryPoint:創(chuàng)建一個(gè)依賴容器凑懂,該容器遵循Android類的生命周期
- @Module:告訴Hilt如何提供不同類型的實(shí)例
- @InstallIn:用來(lái)告訴Hilt這個(gè)模塊會(huì)被安裝到哪個(gè)組件上
- @Provides:告訴Hilt如何獲取具體實(shí)例
- @Singleton:?jiǎn)卫?/li>
- @ViewModelInject:通過(guò)構(gòu)造函數(shù),給ViewModel注入實(shí)例
1.Application注入HiltAndroidApp
@HiltAndroidApp
class APP : Application()
別忘了在Manifest中配置
2.Activity中開始查找注入對(duì)象
使用AndroidEntryPoint注解來(lái)表示梧宫,Hilt開始查找注入對(duì)象
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
3.Hilt注入網(wǎng)絡(luò)模塊
我們準(zhǔn)備使用Retrofit封裝一個(gè)網(wǎng)絡(luò)模塊接谨,需要對(duì)該模塊使用Module注解和InstallIn注解綁定到對(duì)應(yīng)Android類的生命周期,顯然整個(gè)APP運(yùn)行過(guò)程中塘匣,我們都要使用網(wǎng)絡(luò)模塊脓豪,所以選擇綁定Application
@InstallIn(ApplicationComponent::class)
@Module
object RetrofitModule {
}
提供一個(gè)方法給Hilt獲取Okhttp對(duì)象,此方法為單例忌卤,所以使用Provides和Singleton
{
private val TAG: String = RetrofitModule.javaClass.simpleName
@Singleton
@Provides
fun getOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor {
Log.d(TAG, it)
}.apply { level = HttpLoggingInterceptor.Level.BODY }
return OkHttpClient.Builder().addInterceptor(interceptor).build()
}
}
再提供一個(gè)獲取Retrofit的方法:
{
@Singleton
@Provides
fun getRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
完整的網(wǎng)絡(luò)模塊代碼:
const val BASE_URL = "http://192.168.17.114:8080/pagingserver_war/"
@InstallIn(ApplicationComponent::class)
@Module
object RetrofitModule {
private val TAG: String = RetrofitModule.javaClass.simpleName
@Singleton
@Provides
fun getOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor {
Log.d(TAG, it)
}.apply { level = HttpLoggingInterceptor.Level.BODY }
return OkHttpClient.Builder().addInterceptor(interceptor).build()
}
@Singleton
@Provides
fun getRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
三扫夜、接口與實(shí)體類
1.根據(jù)接口和接口返回的json數(shù)據(jù)赡若,分別創(chuàng)建API和實(shí)體類
api地址:ikds.do?since=0&pagesize=5
服務(wù)器數(shù)據(jù):
[
{
"id":1,
"title":"扎克·施奈德版正義聯(lián)盟",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
"rate":"8.9"
},
{
"id":2,
"title":"侍神令",
"cover":"https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2629260713.webp",
"rate":"5.8"
},
{
"id":3,
"title":"雙層肉排",
"cover":"https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2633977758.webp",
"rate":"6.7"
},
{
"id":4,
"title":"大地",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2628845704.webp",
"rate":"6.6"
},
{
"id":5,
"title":"租來(lái)的朋友",
"cover":"https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2616903233.webp",
"rate":"6.1"
}
]
實(shí)體類:
data class MovieItemModel(
val id: Int,
val title: String,
val cover: String,
val rate: String
)
API接口:
interface MovieService {
@GET("ikds.do")
suspend fun getMovieList(
@Query("since") since: Int,
@Query("pagesize") pagesize: Int
): List<MovieItemModel>
}
2.在網(wǎng)絡(luò)模塊RetrofitModule中新增獲取MovieService的方法
{
@Singleton
@Provides
fun provideMovieService(retrofit: Retrofit): MovieService {
return retrofit.create(MovieService::class.java)
}
}
四肃拜、Hilt注入數(shù)據(jù)庫(kù)模塊
1.Room相關(guān)基類
使用Room數(shù)據(jù)庫(kù),首先創(chuàng)建Entity咆课,這邊加了一個(gè)頁(yè)碼的字段:
@Entity
data class MovieEntity(
@PrimaryKey
val id: Int,
val title: String,
val cover: String,
val rate: String,
val page: Int//頁(yè)碼
)
創(chuàng)建Dao棍厂,Room支持返回PagingSource對(duì)象颗味,可以直接和我們的Paging結(jié)合使用了:
@Dao
interface MovieDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(movieList: List<MovieEntity>)
@Query("SELECT * FROM MovieEntity")
fun getMovieList(): PagingSource<Int, MovieEntity>
@Query("DELETE FROM MovieEntity")
suspend fun clear()
}
定義Database抽象類:
@Database(entities = [MovieEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
}
2.Hilt注入數(shù)據(jù)庫(kù)模塊
數(shù)據(jù)庫(kù)模塊同樣需要伴隨應(yīng)用的生命周期,所以還是和Application綁定
提供方法給Hilt獲取AppDatabase牺弹、MovieDao
@InstallIn(ApplicationComponent::class)
@Module
object RoomModule {
@Singleton
@Provides
fun getAppDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(application, AppDatabase::class.java, "my.db")
.build()
}
@Singleton
@Provides
fun provideMovieDao(appDatabase: AppDatabase): MovieDao {
return appDatabase.movieDao()
}
}
五浦马、Pager配置
我們有了網(wǎng)絡(luò)模塊,數(shù)據(jù)庫(kù)模塊张漂,接下來(lái)就要實(shí)現(xiàn)配置Pager晶默,PagingSource我們已經(jīng)實(shí)現(xiàn)了從數(shù)據(jù)庫(kù)獲取,現(xiàn)在需要的實(shí)現(xiàn)的是:網(wǎng)絡(luò)數(shù)據(jù)使用RemoteMediator獲取
1.網(wǎng)絡(luò)數(shù)據(jù)獲染樾狻:RemoteMediator
結(jié)合最初的架構(gòu)圖荤胁,RemoteMediator是用于獲取網(wǎng)絡(luò)數(shù)據(jù),并將數(shù)據(jù)存入數(shù)據(jù)庫(kù)屎债,我們就可以從數(shù)據(jù)庫(kù)獲取PagingSource,傳遞給后續(xù)的Pager
@OptIn(ExperimentalPagingApi::class)
class MovieRemoteMediator(
private val api: MovieService,
private val appDatabase: AppDatabase
) : RemoteMediator<Int, MovieEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MovieEntity>
): MediatorResult {
TODO("Not yet implemented")
}
}
load函數(shù)先放一邊垢油,先來(lái)實(shí)現(xiàn)架構(gòu)中其他模塊
2.對(duì)ViewModel暴露獲取數(shù)據(jù)接口:Repository
定義一個(gè)Repository接口獲取Flow<PagingData<T>>數(shù)據(jù)盆驹,T應(yīng)該為MovieItemModel,因?yàn)閷?duì)外(ViewModel)而言滩愁,使用的都是MovieItemModel網(wǎng)絡(luò)對(duì)象躯喇,對(duì)內(nèi)使用的才是MovieEntity數(shù)據(jù)庫(kù)對(duì)象
interface Repository<T : Any> {
fun fetchList(): Flow<PagingData<T>>
}
實(shí)現(xiàn)類,使用MovieItemModel作為泛型類型,并返回Pager的Flow:
class MovieRepositoryImpl(
private val api: MovieService,
private val appDatabase: AppDatabase
) : Repository<MovieItemModel> {
override fun fetchList(): Flow<PagingData<MovieItemModel>> {
val pageSize = 10
return Pager(
config = PagingConfig(
initialLoadSize = pageSize * 2,
pageSize = pageSize,
prefetchDistance = 1
),
remoteMediator = MovieRemoteMediator(api, appDatabase)
) {
appDatabase.movieDao().getMovieList()
}.flow.flowOn(Dispatchers.IO).map {
}
}
}
編譯器上可以看到map中的it對(duì)象為Paging<MovieEntity>類型的廉丽,因?yàn)槲覀?strong>MovieDao返回的是一個(gè)PagingSource<Int, MovieEntity>對(duì)象倦微,所以需要把MovieEntity轉(zhuǎn)換為MovieItemModel
3.Data Mapper
Data Mapper廣泛應(yīng)用于MyBatis,Data Mapper將數(shù)據(jù)源的Model(MovieEntity)轉(zhuǎn)換為頁(yè)面顯示Model(MovieItemModel)正压,兩者分開的原因就是為了Model層和View層進(jìn)一步解耦
定義統(tǒng)一轉(zhuǎn)換接口:
interface Mapper<I, O> {
fun map(input: I): O
}
針對(duì)MovieEntity和MovieItemModel實(shí)現(xiàn)接口
class MovieEntity2ItemModelMapper : Mapper<MovieEntity, MovieItemModel> {
override fun map(input: MovieEntity): MovieItemModel {
return input.run {
MovieItemModel(
id = id,
title = title,
cover = cover,
rate = rate
)
}
}
}
4.利用Mapper對(duì)Repository轉(zhuǎn)換
有了Mapper后欣福,就可以將2.中我們的MovieEntity轉(zhuǎn)換為MovieItemModel了
class MovieRepositoryImpl(
private val api: MovieService,
private val appDatabase: AppDatabase,
private val mapper: MovieEntity2ItemModelMapper
) : Repository<MovieItemModel> {
@OptIn(ExperimentalPagingApi::class)
override fun fetchList(): Flow<PagingData<MovieItemModel>> {
val pageSize = 10
return Pager(
config = PagingConfig(
initialLoadSize = pageSize * 2,
pageSize = pageSize,
prefetchDistance = 1
),
remoteMediator = MovieRemoteMediator(api, appDatabase)
) {
appDatabase.movieDao().getMovieList()
}.flow.flowOn(Dispatchers.IO).map { pagingData ->
pagingData.map { mapper.map(it) }
}
}
}
5.Hilt注入Repository
Repository的生命周期并不是伴隨應(yīng)用的,而是伴隨Activity焦履,所以安裝到ActivityComponent
同樣方法也不是單例的拓劝,而是根據(jù)Activity,使用ActivityScoped注解
@InstallIn(ActivityComponent::class)
@Module
object RepositoryModule {
@ActivityScoped
@Provides
fun provideMovieRepository(
api: MovieService,
appDatabase: AppDatabase
): MovieRepositoryImpl {
return MovieRepositoryImpl(api, appDatabase, MovieEntity2ItemModelMapper())
}
}
六嘉裤、ViewModel
Model層的架構(gòu)搭建完畢后郑临,我們需要ViewModel層與Model層作數(shù)據(jù)交互
Hilt注入ViewModel構(gòu)造函數(shù)
ViewModel中需要Repository對(duì)象作為屬性,而Hilt支持使用ViewModelInject注解給ViewModel構(gòu)造函數(shù)注入
class MovieViewModel @ViewModelInject constructor(
private val repository: MovieRepositoryImpl
) : ViewModel() {
val data = repository.fetchList().cachedIn(viewModelScope).asLiveData()
}
七屑宠、Adapter與Coil
ViewModel完成后厢洞,接下來(lái)需要RecyclerView的Adapter,這塊和之前的Paggin3一樣
1.布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dip">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dip"
android:layout_height="100dip"
app:image="@{movie.cover}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.432"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.054"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/textViewTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{movie.title}"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.255"
tools:text="泰坦尼克號(hào)" />
<TextView
android:id="@+id/textViewRate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@{movie.rate}"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textViewTitle"
tools:text="評(píng)分:8.9分" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.4" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
<data>
<variable
name="movie"
type="com.aruba.mvvmapplication.model.MovieItemModel" />
</data>
</layout>
2.BindingAdapter
使用BindingAdapter自定義一個(gè)image屬性
這邊選用Coil作為圖片加載框架典奉,Coil相較于其他框架擁有更好的性能躺翻、更小的體積、易用性秋柄、結(jié)合了協(xié)程获枝、androidx等最新技術(shù)、還擁有緩存骇笔、動(dòng)態(tài)采樣省店、加載暫停/終止等功能
@BindingAdapter("image")
fun setImage(imageView: ImageView, imageUrl: String) {
imageView.load(imageUrl) {
placeholder(R.drawable.ic_launcher_foreground)//占位圖
crossfade(true)//淡入淡出
}
}
3.Adapter實(shí)現(xiàn)
使用ViewDataBinding作為屬性,定義一個(gè)基類ViewHolder
class BindingViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
Adapter繼承PagingDataAdapter笨触,并傳入一個(gè)DiffUtil.ItemCallback
class MoviePagingAdapter : PagingDataAdapter<MovieItemModel, BindingViewHolder>(
object : DiffUtil.ItemCallback<MovieItemModel>() {
override fun areItemsTheSame(oldItem: MovieItemModel, newItem: MovieItemModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MovieItemModel, newItem: MovieItemModel): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingViewHolder(binding)
}
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
if (getItem(position) != null)
(holder.binding as ItemBinding).movie = getItem(position)
}
}
4.為RecyclerView添加擴(kuò)展函數(shù)
為了后續(xù)Paging的使用懦傍,為RecyclerView添加設(shè)置Adapter和liveData的擴(kuò)展函數(shù):
fun <VH : RecyclerView.ViewHolder, T : Any> RecyclerView.setPagingAdapter(
owner: LifecycleOwner,
adapter: PagingDataAdapter<T, VH>,
liveData: LiveData<PagingData<T>>
) {
liveData.observe(owner) {
adapter.submitData(owner.lifecycle, it)
}
}
Activity的代碼如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private val viewModel: MovieViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.recyclerview.setPagingAdapter(
owner = this,
adapter = MoviePagingAdapter(),
liveData = viewModel.data
)
}
}
八、實(shí)現(xiàn)RemoteMediator
之前未實(shí)現(xiàn)load函數(shù)的代碼:
@OptIn(ExperimentalPagingApi::class)
class MovieRemoteMediator(
private val api: MovieService,
private val appDatabase: AppDatabase
) : RemoteMediator<Int, MovieEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MovieEntity>
): MediatorResult {
TODO("Not yet implemented")
}
}
1.MediatorResult
load函數(shù)需要一個(gè)MediatorResult類型的返回值芦劣,MediatorResult有三種返回參數(shù):
- MediatorResult.Error(e):出現(xiàn)錯(cuò)誤
- MediatorResult.Success(endOfPaginationReached = false):請(qǐng)求成功且有數(shù)據(jù)(還有下一頁(yè))
- MediatorResult.Success(endOfPaginationReached = true):請(qǐng)求成功但沒有數(shù)據(jù)(到底了)
返回MediatorResult.Success粗俱,pager就會(huì)從數(shù)據(jù)庫(kù)中拿數(shù)據(jù),load函數(shù)初步實(shí)現(xiàn):
{
try {
//1.判斷l(xiāng)oadType
//2.請(qǐng)求網(wǎng)絡(luò)分頁(yè)數(shù)據(jù)
//3.存入數(shù)據(jù)庫(kù)
val endOfPaginationReached = true
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
2.LoadType
LoadType為枚舉類虚吟,有三個(gè)對(duì)象:
- Refresh:首次加載數(shù)據(jù)和調(diào)用PagingDataAdapter.refresh()時(shí)觸發(fā)
- Append:加載更多數(shù)據(jù)時(shí)觸發(fā)
- Prepend:在列表頭部添加數(shù)據(jù)時(shí)觸發(fā)寸认,Refresh觸發(fā)時(shí)也會(huì)觸發(fā)
第一步就需要判斷LoadType的狀態(tài),如果是Refresh串慰,那么數(shù)據(jù)庫(kù)中沒有數(shù)據(jù)偏塞,就要從網(wǎng)絡(luò)獲取數(shù)據(jù),Refresh狀態(tài)下load函數(shù)執(zhí)行完畢后會(huì)自動(dòng)再次調(diào)用load函數(shù)邦鲫,此時(shí)的LoadType為Append灸叼,此時(shí)數(shù)據(jù)庫(kù)中有數(shù)據(jù)了神汹,直接返回Success通知Pager可以從數(shù)據(jù)庫(kù)取數(shù)據(jù)了
{
try {
//1.判斷l(xiāng)oadType
val pageKey = when (loadType) {
//首次加載
LoadType.REFRESH -> null
//REFRESH之后還會(huì)調(diào)用load(REFRESH時(shí)數(shù)據(jù)庫(kù)中沒有數(shù)據(jù)),來(lái)加載開頭的數(shù)據(jù)古今,直接返回成功就可以了
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false)
//加載更多
LoadType.APPEND -> {
}
}
//2.請(qǐng)求網(wǎng)絡(luò)分頁(yè)數(shù)據(jù)
val page = pageKey ?: 0
//3.存入數(shù)據(jù)庫(kù)
val endOfPaginationReached = true
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
3.PagingState
對(duì)于下一頁(yè)的數(shù)據(jù)屁魏,則要使用PagingState獲取了,PagingState分為兩部分組成:
- pages:上一頁(yè)的數(shù)據(jù)捉腥,主要用來(lái)獲取最后一個(gè)item氓拼,作為下一頁(yè)的開始位置
- config:配置Pager時(shí)的PagingConfig,可以獲取到pageSize等一系列初始化配置的值
如果上一頁(yè)最后一個(gè)item為空但狭,那么表示列表加載到底了披诗,否則獲取到需要加載的當(dāng)前page
{
//加載更多
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success(
endOfPaginationReached = true
)
lastItem.page//返回當(dāng)前頁(yè)
}
}
4.網(wǎng)絡(luò)獲取數(shù)據(jù)和存入數(shù)據(jù)庫(kù)
接下來(lái)就是從網(wǎng)絡(luò)獲取數(shù)據(jù)了:
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MovieEntity>
): MediatorResult {
try {
//1.判斷l(xiāng)oadType
val pageKey = when (loadType) {
//首次加載
LoadType.REFRESH -> null
//REFRESH之后還會(huì)調(diào)用load(REFRESH時(shí)數(shù)據(jù)庫(kù)中沒有數(shù)據(jù)),來(lái)加載開頭的數(shù)據(jù)立磁,直接返回成功就可以了
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false)
//加載更多
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success(
endOfPaginationReached = true
)
lastItem.page//返回當(dāng)前頁(yè)
}
}
//2.請(qǐng)求網(wǎng)絡(luò)分頁(yè)數(shù)據(jù)
val page = pageKey ?: 0
val result = api.getMovieList(
page * state.config.pageSize,
state.config.pageSize
)
//3.存入數(shù)據(jù)庫(kù)
val endOfPaginationReached = true
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
將服務(wù)器對(duì)象轉(zhuǎn)換為本地?cái)?shù)據(jù)庫(kù)對(duì)象后呈队,存入數(shù)據(jù)庫(kù),完整RemoteMediator代碼:
@OptIn(ExperimentalPagingApi::class)
class MovieRemoteMediator(
private val api: MovieService,
private val appDatabase: AppDatabase
) : RemoteMediator<Int, MovieEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MovieEntity>
): MediatorResult {
try {
//1.判斷l(xiāng)oadType
val pageKey = when (loadType) {
//首次加載
LoadType.REFRESH -> null
//REFRESH之后還會(huì)調(diào)用load(REFRESH時(shí)數(shù)據(jù)庫(kù)中沒有數(shù)據(jù))唱歧,來(lái)加載開頭的數(shù)據(jù)宪摧,直接返回成功就可以了
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false)
//加載更多
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success(
endOfPaginationReached = true
)
lastItem.page//返回當(dāng)前頁(yè)
}
}
//2.請(qǐng)求網(wǎng)絡(luò)分頁(yè)數(shù)據(jù)
val page = pageKey ?: 0
val result = api.getMovieList(
page * state.config.pageSize,
state.config.pageSize
)
//服務(wù)器對(duì)象轉(zhuǎn)換為本地?cái)?shù)據(jù)庫(kù)對(duì)象
val entity = result.map {
MovieEntity(
id = it.id,
title = it.title,
cover = it.cover,
rate = it.rate,
page = page + 1
)
}
//3.存入數(shù)據(jù)庫(kù)
val movieDao = appDatabase.movieDao()
appDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
movieDao.clear()
}
movieDao.insert(entity)
}
val endOfPaginationReached = result.isEmpty()
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
}
運(yùn)行后的效果:
九、刷新
1.上拉刷新颅崩、重試按鈕几于、錯(cuò)誤信息
上拉刷新、重試按鈕沿后、錯(cuò)誤信息布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="20dp">
<Button
android:id="@+id/retryButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/footer_retry"
android:textColor="@android:color/background_dark" />
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/errorMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/background_dark"
tools:text="連接超時(shí)"/>
</LinearLayout>
之前我們使用Paging的LoadStateAdapter沿彭,直接設(shè)置到PagingDataAdapter上就可以了,刷新對(duì)應(yīng)的ViewHolder如下:
class NetWorkStateItemViewHolder(
private val binding: NetworkStateItemBinding,
val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bindData(data: LoadState){
binding.apply {
// 正在加載尖滚,顯示進(jìn)度條
progress.isVisible = data is LoadState.Loading
// 加載失敗喉刘,顯示并點(diǎn)擊重試按鈕
retryButton.isVisible = data is LoadState.Error
retryButton.setOnClickListener { retryCallback() }
// 加載失敗顯示錯(cuò)誤原因
errorMsg.isVisible = !(data as? LoadState.Error)?.error?.message.isNullOrBlank()
errorMsg.text = (data as? LoadState.Error)?.error?.message
}
}
}
inline var View.isVisible: Boolean
get() = visibility == View.VISIBLE
set(value) {
visibility = if (value) View.VISIBLE else View.GONE
}
Adapter代碼:
class FooterAdapter(
val adapter: MoviePagingAdapter
) : LoadStateAdapter<NetWorkStateItemViewHolder>() {
override fun onBindViewHolder(holder: NetWorkStateItemViewHolder, loadState: LoadState) {
//水平居中
val params = holder.itemView.layoutParams
if (params is StaggeredGridLayoutManager.LayoutParams) {
params.isFullSpan = true
}
holder.bindData(loadState)
}
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): NetWorkStateItemViewHolder {
val binding =
NetworkStateItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return NetWorkStateItemViewHolder(binding) { adapter.retry() }
}
}
Activity中配置下PagingDataAdapter,并為RecyclerView設(shè)置ConcatAdapter漆弄,一定要設(shè)置成withLoadStateFooter函數(shù)返回的Adapter睦裳,否則不會(huì)有效果!撼唾!
val adapter = MoviePagingAdapter()
binding.recyclerview.adapter = adapter
.run { withLoadStateFooter(FooterAdapter(this)) }
2.下拉刷新
下拉刷新和之前也是相同的廉邑,布局中嵌套一個(gè)SwipeRefreshLayout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity中對(duì)PagingDataAdapter的loadState進(jìn)行監(jiān)聽:
lifecycleScope.launchWhenCreated {
//監(jiān)聽adapter狀態(tài)
adapter.loadStateFlow.collect {
//根據(jù)刷新狀態(tài)來(lái)通知swiprefreshLayout是否刷新完畢
binding.refreshLayout.isRefreshing = it.refresh is LoadState.Loading
}
}
十、App Starup實(shí)現(xiàn)無(wú)網(wǎng)絡(luò)數(shù)據(jù)組件初始化
RemoteMediator中可以在無(wú)網(wǎng)絡(luò)時(shí)從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)倒谷,所以load函數(shù)中我們還需要對(duì)網(wǎng)絡(luò)狀態(tài)進(jìn)行判斷蛛蒙,無(wú)網(wǎng)絡(luò)時(shí),直接返回Success
1.獲取網(wǎng)絡(luò)狀態(tài)的擴(kuò)展函數(shù)
定義一個(gè)擴(kuò)展函數(shù)用來(lái)獲取網(wǎng)絡(luò)狀態(tài):
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
fun Context.isConnectedNetwork(): Boolean = run {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
activeNetwork?.isConnectedOrConnecting == true
}
Manifest中不要忘了加權(quán)限
2.新建幫助類渤愁,初始化Context
object AppHelper {
lateinit var mContext: Context
fun init(context: Context) {
this.mContext = context
}
}
3.RemoteMediator中判斷網(wǎng)絡(luò)狀態(tài)并返回
//無(wú)網(wǎng)絡(luò)從本地?cái)?shù)據(jù)庫(kù)獲取數(shù)據(jù)
if (!AppHelper.mContext.isConnectedNetwork()) {
return MediatorResult.Success(endOfPaginationReached = false)
}
此時(shí)AppHelper的init函數(shù)還沒有調(diào)用
4.App Starup
App Starup是JetPack的新成員宇驾,提供了在App啟動(dòng)時(shí)初始化組件簡(jiǎn)單、高效的方法猴伶,還可以指定初始化順序,我們新建一個(gè)類繼承于Initializer:
class AppInitializer : Initializer<Unit> {
override fun create(context: Context) {
AppHelper.init(context)
}
//按順序執(zhí)行初始化
override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}
最后還需要在Manifest中注冊(cè):
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.aruba.mvvmapplication.init.AppInitializer"
android:value="androidx.startup" />
</provider>
最終效果: