前面我们使用Java来运用JetPack中的一系列组件,又使用kotlin运用这些组件实现了一系列功能:
接着,Jetpack的Paging3中,我们使用的语言是kotlin,相信通过这些项目的对比,你就能发现koltin取代Java的理由了,kotlin拥有更好的扩展性,更高的性能,更简洁的代码,更好的Jetpack组件支持,如果你还对kotlin不熟悉,那么可以查阅我的kotlin专题博客,在此也要感谢动脑学院Jason老师的辛勤付出,动脑学院在B站上也有投稿koltin基础的视频,通过视频可以快速学习和上手kotlin
今天来综合使用各种组件,搭建最新MVVM项目框架,利用Paging3实现列表功能,Paging3和Paging2一样,支持数据库缓存
一、依赖
主项目gradle中导入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编译会出现问题: 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:触发Hilt的代码生成
- @AndroidEntryPoint:创建一个依赖容器,该容器遵循Android类的生命周期
- @Module:告诉Hilt如何提供不同类型的实例
- @InstallIn:用来告诉Hilt这个模块会被安装到哪个组件上
- @Provides:告诉Hilt如何获取具体实例
- @Singleton:单例
- @ViewModelInject:通过构造函数,给ViewModel注入实例
1.Application注入HiltAndroidApp
@HiltAndroidApp class APP : Application()
别忘了在Manifest中配置
2.Activity中开始查找注入对象
使用AndroidEntryPoint注解来表示,Hilt开始查找注入对象
@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注入网络模块
我们准备使用Retrofit封装一个网络模块,需要对该模块使用Module注解和InstallIn注解绑定到对应Android类的生命周期,显然整个APP运行过程中,我们都要使用网络模块,所以选择绑定Application
@InstallIn(ApplicationComponent::class) @Module object RetrofitModule { }
提供一个方法给Hilt获取Okhttp对象,此方法为单例,所以使用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() } }
再提供一个获取Retrofit的方法:
{ @Singleton @Provides fun getRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() } }
完整的网络模块代码:
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() } }
三、接口与实体类
1.根据接口和接口返回的json数据,分别创建API和实体类
api地址:ikds.do?since=0&pagesize=5 服务器数据:
[ { "id":1, "title":"扎克·施奈德版正义联盟", "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":"租来的朋友", "cover":"https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2616903233.webp", "rate":"6.1" } ]
实体类:
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.在网络模块RetrofitModule中新增获取MovieService的方法
{ @Singleton @Provides fun provideMovieService(retrofit: Retrofit): MovieService { return retrofit.create(MovieService::class.java) } }
四、Hilt注入数据库模块
1.Room相关基类
使用Room数据库,首先创建Entity,这边加了一个页码的字段:
@Entity data class MovieEntity( @PrimaryKey val id: Int, val title: String, val cover: String, val rate: String, val page: Int//页码 )
创建Dao,Room支持返回PagingSource对象,可以直接和我们的Paging结合使用了:
@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注入数据库模块
数据库模块同样需要伴随应用的生命周期,所以还是和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配置
我们有了网络模块,数据库模块,接下来就要实现配置Pager,PagingSource我们已经实现了从数据库获取,现在需要的实现的是:网络数据使用RemoteMediator获取
1.网络数据获取:RemoteMediator
结合最初的架构图,RemoteMediator是用于获取网络数据,并将数据存入数据库,我们就可以从数据库获取PagingSource,传递给后续的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函数先放一边,先来实现架构中其他模块
2.对ViewModel暴露获取数据接口:Repository
定义一个Repository接口获取Flow<PagingData<T>>数据,T应该为MovieItemModel,因为对外(ViewModel)而言,使用的都是MovieItemModel网络对象,对内使用的才是MovieEntity数据库对象
interface Repository<T : Any> { fun fetchList(): Flow<PagingData<T>> }
实现类,使用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对象为Paging<MovieEntity>类型的,因为我们MovieDao返回的是一个PagingSource<Int, MovieEntity>对象,所以需要把MovieEntity转换为MovieItemModel
3.Data Mapper
Data Mapper广泛应用于MyBatis,Data Mapper将数据源的Model(MovieEntity)转换为页面显示Model(MovieItemModel),两者分开的原因就是为了Model层和View层进一步解耦
定义统一转换接口:
interface Mapper<I, O> { fun map(input: I): O }
针对MovieEntity和MovieItemModel实现接口
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对Repository转换
有了Mapper后,就可以将2.中我们的MovieEntity转换为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的生命周期并不是伴随应用的,而是伴随Activity,所以安装到ActivityComponent 同样方法也不是单例的,而是根据Activity,使用ActivityScoped注解
@InstallIn(ActivityComponent::class) @Module object RepositoryModule { @ActivityScoped @Provides fun provideMovieRepository( api: MovieService, appDatabase: AppDatabase ): MovieRepositoryImpl { return MovieRepositoryImpl(api, appDatabase, MovieEntity2ItemModelMapper()) } }
六、ViewModel
Model层的架构搭建完毕后,我们需要ViewModel层与Model层作数据交互
Hilt注入ViewModel构造函数
ViewModel中需要Repository对象作为属性,而Hilt支持使用ViewModelInject注解给ViewModel构造函数注入
class MovieViewModel @ViewModelInject constructor( private val repository: MovieRepositoryImpl ) : ViewModel() { val data = repository.fetchList().cachedIn(viewModelScope).asLiveData() }
七、Adapter与Coil
ViewModel完成后,接下来需要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="泰坦尼克号" /> <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="评分: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自定义一个image属性 这边选用Coil作为图片加载框架,Coil相较于其他框架拥有更好的性能、更小的体积、易用性、结合了协程、androidx等最新技术、还拥有缓存、动态采样、加载暂停/终止等功能
@BindingAdapter("image") fun setImage(imageView: ImageView, imageUrl: String) { imageView.load(imageUrl) { placeholder(R.drawable.ic_launcher_foreground)//占位图 crossfade(true)//淡入淡出 } }
3.Adapter实现
使用ViewDataBinding作为属性,定义一个基类ViewHolder
class BindingViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
Adapter继承PagingDataAdapter,并传入一个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添加扩展函数
为了后续Paging的使用,为RecyclerView添加设置Adapter和liveData的扩展函数:
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 ) } }
八、实现RemoteMediator
之前未实现load函数的代码:
@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函数需要一个MediatorResult类型的返回值,MediatorResult有三种返回参数:
- MediatorResult.Error(e):出现错误
- MediatorResult.Success(endOfPaginationReached = true):请求成功且有数据(还有下一页)
- MediatorResult.Success(endOfPaginationReached = false):请求成功但没有数据(到底了)
返回MediatorResult.Success,pager就会从数据库中拿数据,load函数初步实现:
{ try { //1.判断loadType //2.请求网络分页数据 //3.存入数据库 val endOfPaginationReached = true return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { return MediatorResult.Error(e) } }
2.LoadType
LoadType为枚举类,有三个对象:
- Refresh:首次加载数据和调用PagingDataAdapter.refresh()时触发
- Append:加载更多数据时触发
- Prepend:在列表头部添加数据时触发,Refresh触发时也会触发
第一步就需要判断LoadType的状态,如果是Refresh,那么数据库中没有数据,就要从网络获取数据,Refresh状态下load函数执行完毕后会自动再次调用load函数,此时的LoadType为Append,此时数据库中有数据了,直接返回Success通知Pager可以从数据库取数据了
{ try { //1.判断loadType val pageKey = when (loadType) { //首次加载 LoadType.REFRESH -> null //REFRESH之后还会调用load(REFRESH时数据库中没有数据),来加载开头的数据,直接返回成功就可以了 LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false) //加载更多 LoadType.APPEND -> { } } //2.请求网络分页数据 val page = pageKey ?: 0 //3.存入数据库 val endOfPaginationReached = true return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { return MediatorResult.Error(e) } }
3.PagingState
对于下一页的数据,则要使用PagingState获取了,PagingState分为两部分组成:
- pages:上一页的数据,主要用来获取最后一个item,作为下一页的开始位置
- config:配置Pager时的PagingConfig,可以获取到pageSize等一系列初始化配置的值
如果上一页最后一个item为空,那么表示列表加载到底了,否则获取到需要加载的当前page
{ //加载更多 LoadType.APPEND -> { val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success( endOfPaginationReached = true ) lastItem.page//返回当前页 } }
4.网络获取数据和存入数据库
接下来就是从网络获取数据了:
override suspend fun load( loadType: LoadType, state: PagingState<Int, MovieEntity> ): MediatorResult { try { //1.判断loadType val pageKey = when (loadType) { //首次加载 LoadType.REFRESH -> null //REFRESH之后还会调用load(REFRESH时数据库中没有数据),来加载开头的数据,直接返回成功就可以了 LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false) //加载更多 LoadType.APPEND -> { val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success( endOfPaginationReached = true ) lastItem.page//返回当前页 } } //2.请求网络分页数据 val page = pageKey ?: 0 val result = api.getMovieList( page * state.config.pageSize, state.config.pageSize ) //3.存入数据库 val endOfPaginationReached = true return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { return MediatorResult.Error(e) } }
将服务器对象转换为本地数据库对象后,存入数据库,完整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.判断loadType val pageKey = when (loadType) { //首次加载 LoadType.REFRESH -> null //REFRESH之后还会调用load(REFRESH时数据库中没有数据),来加载开头的数据,直接返回成功就可以了 LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = false) //加载更多 LoadType.APPEND -> { val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success( endOfPaginationReached = true ) lastItem.page//返回当前页 } } //2.请求网络分页数据 val page = pageKey ?: 0 val result = api.getMovieList( page * state.config.pageSize, state.config.pageSize ) //服务器对象转换为本地数据库对象 val entity = result.map { MovieEntity( id = it.id, title = it.title, cover = it.cover, rate = it.rate, page = page + 1 ) } //3.存入数据库 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) } } }
运行后的效果:
联动.gif
九、刷新
1.上拉刷新、重试按钮、错误信息
上拉刷新、重试按钮、错误信息布局文件如下:
<?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="连接超时"/> </LinearLayout>
之前我们使用Paging的LoadStateAdapter,直接设置到PagingDataAdapter上就可以了,刷新对应的ViewHolder如下:
class NetWorkStateItemViewHolder( private val binding: NetworkStateItemBinding, val retryCallback: () -> Unit ) : RecyclerView.ViewHolder(binding.root) { fun bindData(data: LoadState){ binding.apply { // 正在加载,显示进度条 progress.isVisible = data is LoadState.Loading // 加载失败,显示并点击重试按钮 retryButton.isVisible = data is LoadState.Error retryButton.setOnClickListener { retryCallback() } // 加载失败显示错误原因 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设置ConcatAdapter,一定要设置成withLoadStateFooter函数返回的Adapter,否则不会有效果!!
val adapter = MoviePagingAdapter() binding.recyclerview.adapter = adapter .run { withLoadStateFooter(FooterAdapter(this)) }
2.下拉刷新
下拉刷新和之前也是相同的,布局中嵌套一个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中对PagingDataAdapter的loadState进行监听:
lifecycleScope.launchWhenCreated { //监听adapter状态 adapter.loadStateFlow.collect { //根据刷新状态来通知swiprefreshLayout是否刷新完毕 binding.refreshLayout.isRefreshing = it.refresh is LoadState.Loading } }
十、App Starup实现无网络数据组件初始化
RemoteMediator中可以在无网络时从数据库获取数据,所以load函数中我们还需要对网络状态进行判断,无网络时,直接返回Success
1.获取网络状态的扩展函数
定义一个扩展函数用来获取网络状态:
@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中不要忘了加权限
2.新建帮助类,初始化Context
object AppHelper { lateinit var mContext: Context fun init(context: Context) { this.mContext = context } }
3.RemoteMediator中判断网络状态并返回
//无网络从本地数据库获取数据 if (!AppHelper.mContext.isConnectedNetwork()) { return MediatorResult.Success(endOfPaginationReached = false) }
此时AppHelper的init函数还没有调用
4.App Starup
image.png
App Starup是JetPack的新成员,提供了在App启动时初始化组件简单、高效的方法,还可以指定初始化顺序,我们新建一个类继承于Initializer:
class AppInitializer : Initializer<Unit> { override fun create(context: Context) { AppHelper.init(context) } //按顺序执行初始化 override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf() }
最后还需要在Manifest中注册:
<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>
最终效果: