前面我们使用Java来运用JetPack中的一系列组件,又使用kotlin运用这些组件实现了一系列功能:

  • kotlin--Flow文件下载
  • kotlin--Flow结合Room运用
  • kotlin--Flow结合retrofit运用
  • kotlin--StateFlow运用
  • kotlin--SharedFlow运用

接着,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 = trueviewBinding = 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@Providesfun getOkHttpClient(): OkHttpClient {val interceptor = HttpLoggingInterceptor {Log.d(TAG, it)}.apply { level = HttpLoggingInterceptor.Level.BODY }return OkHttpClient.Builder().addInterceptor(interceptor).build()}
}

再提供一个获取Retrofit的方法:

{@Singleton@Providesfun getRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(BASE_URL).client(OkHttpClient.Builder().build()).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@Providesfun getOkHttpClient(): OkHttpClient {val interceptor = HttpLoggingInterceptor {Log.d(TAG, it)}.apply { level = HttpLoggingInterceptor.Level.BODY }return OkHttpClient.Builder().addInterceptor(interceptor).build()}@Singleton@Providesfun getRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(BASE_URL).client(OkHttpClient.Builder().build()).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@Providesfun provideMovieService(retrofit: Retrofit): MovieService {return retrofit.create(MovieService::class.java)}
}

四、Hilt注入数据库模块

1.Room相关基类

使用Room数据库,首先创建Entity,这边加了一个页码的字段:

@Entity
data class MovieEntity(@PrimaryKeyval 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@Providesfun getAppDatabase(application: Application): AppDatabase {return Room.databaseBuilder(application, AppDatabase::class.java, "my.db").build()}@Singleton@Providesfun 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 = 10return 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 = 10return 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@Providesfun 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.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:paddingVertical="10dip"><ImageViewandroid: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" /><TextViewandroid: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="泰坦尼克号" /><TextViewandroid: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.Guidelineandroid: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.Guidelineandroid: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><variablename="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 = false):请求成功且有数据(还有下一页)
  • MediatorResult.Success(endOfPaginationReached = true):请求成功但没有数据(到底了)

返回MediatorResult.Success,pager就会从数据库中拿数据,load函数初步实现:

{try {//1.判断loadType//2.请求网络分页数据//3.存入数据库val endOfPaginationReached = truereturn 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.判断loadTypeval 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 = truereturn 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.判断loadTypeval 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 ?: 0val result = api.getMovieList(page * state.config.pageSize,state.config.pageSize)//3.存入数据库val endOfPaginationReached = truereturn 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.判断loadTypeval 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 ?: 0val 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"><Buttonandroid: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" /><ProgressBarandroid:id="@+id/progress"style="?android:attr/progressBarStyle"android:layout_width="match_parent"android:layout_height="wrap_content" /><TextViewandroid: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.ErrorretryButton.setOnClickListener { retryCallback() }// 加载失败显示错误原因errorMsg.isVisible = !(data as? LoadState.Error)?.error?.message.isNullOrBlank()errorMsg.text = (data as? LoadState.Error)?.error?.message}}}inline var View.isVisible: Booleanget() = visibility == View.VISIBLEset(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.layoutParamsif (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.SwipeRefreshLayoutandroid:id="@+id/refreshLayout"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.recyclerview.widget.RecyclerViewandroid: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 ConnectivityManagerval activeNetwork: NetworkInfo? = cm.activeNetworkInfoactiveNetwork?.isConnectedOrConnecting == true
}

Manifest中不要忘了加权限

2.新建帮助类,初始化Context

object AppHelper {lateinit var mContext: Contextfun 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中注册:

<providerandroid:name="androidx.startup.InitializationProvider"android:authorities="${applicationId}.androidx-startup"android:exported="false"tools:node="merge"><meta-dataandroid:name="com.aruba.mvvmapplication.init.AppInitializer"android:value="androidx.startup" /></provider>

最终效果:

项目地址:https://gitee.com/aruba/mvvmapplication.git

kotlin--综合运用Hilt、Paging3、Flow、Room、Retrofit、Coil等实现MVVM架构实战相关推荐

  1. 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

    前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...

  2. sharedpreferences使用方法_Google 推荐在 MVVM 架构中使用 Kotlin Flow

    前言 在之前分享过一篇 Jetpack 综合实战应用 Jetpack 实战:神奇宝贝 ,这个项目主要包了以下功能: 自定义 RemoteMediator 实现 network + db 的混合使用 ( ...

  3. Android Flow遇见Retrofit网络请求实践

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/121754941 本文出自[赵彦军的博客] 文章目录 前言 RetrofitFlow ...

  4. Android Kotlin Paging3 Flow完整教程

    准备好接口 package com.example.android_learn_paging.netimport com.example.android_learn_paging.model.NetD ...

  5. Kotlin替换Dagger2/Hilt的依赖注入框架--Koin。

    Koin.Dagger2.Hilt 目前都是非常流行的库,面对这么多层出不穷的新技术,我们该做如何选择,是一直困扰我们的一个问题. Hilt 与 Dagger2 区别并不大,Hilt就是对Dagger ...

  6. android仿微信红包动画、Kotlin综合应用、Xposed模块、炫酷下拉视觉、UC浏览器滑动动画等源码...

    Android精选源码 仿微信打开红包旋转动画 使用Kotlin编写的Android应用,内容你想象不到 Android手机上的免Root Android系统日志Viewer 一个能让微信 Mater ...

  7. android仿微信红包动画、Kotlin综合应用、Xposed模块、炫酷下拉视觉、UC浏览器滑动动画等源码

    Android精选源码 仿微信打开红包旋转动画 使用Kotlin编写的Android应用,内容你想象不到 Android手机上的免Root Android系统日志Viewer 一个能让微信 Mater ...

  8. Bootstrap4+MySQL前后端综合实训-Day05-AM【MySQL数据库(SQLyog软件基本操作、架构设计器)、eclipse(JDBC开发-添加驱动、构建路径、增删改查基本测试)】

    [Bootstrap4前端框架+MySQL数据库]前后端综合实训[10天课程 博客汇总表 详细笔记] 目   录 MySQL数据库--建库.建表 新建连接.测试连接 新建news_manager数据库 ...

  9. 0.高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

    0.系列文章目录 1.启动界面 2.广告和引导界面 1.项目简介 这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内 ...

最新文章

  1. Ubuntu下修改DNS重启也能用的方法
  2. PHP输出表格的方法
  3. 8.tomcat认证访问
  4. K8S创建role命令示例
  5. 早期计算机音乐创作的歌曲,14.计算机音乐创作(专业组)
  6. tcp_handle_req: Made 4 read attempts but message is not complete yet - closing connection
  7. Apache-DBUtils实现CRUD操作,已封装的API实现jdbc对数据库进行操作
  8. Java中的null是什么?
  9. 计算机辅助项目管理课程方案,天津大学计算机辅助管理研究生课程简介
  10. php 留言板项目 ajax,PHP Ajax留言板
  11. HTML5类选择器使用,CSS选择器种类及使用方法
  12. 如何升级PowerShell
  13. 介绍一种策略分析师必备的解题技巧
  14. 带你玩转JavaWeb开发之四 -如何用JS做登录注册页面校验
  15. UITableView的复用过程
  16. 双线性对在密码学中的应用(下)
  17. ensp 华为路由器配置远程登陆(telnet,ssh)
  18. 哥伦比亚大学 Schulzrinne 教授:撰写科研论文详细教程
  19. 如何建设一个集团网站
  20. Kotlin中三元运算符

热门文章

  1. No connection could be made because the target machine actively refused问题原因解决
  2. 解决Testflight无法接入App Store connect的4种方法
  3. 自定义九宫格解锁控件
  4. Javasocket服务端程序
  5. embedding技术实践
  6. C state和P state 一些体会
  7. linux离线部署fastdfs
  8. Java8 Stream List<Bean> 或List<Map> 转Map
  9. 苹果授权登录(Apple sign in)Java服务端验证通过
  10. 【华为OD】| 数据最节约的备份方法