Android—Flow与Jetpack Paging3

前言

在上一篇中,主要讲解了Jetpack—Paging2的故事。因为Paging3改动较大,并且为了让更多人同时适应两个版本,因此在本篇中将会结合Flow与Paging3进行组合讲解。

因本篇主要围绕着Paging3以及与Paging2的区别进行讲解!

因此,本篇所需要知识点:

  • ViewBinding+DataBinding+Flow+ViewModel+KT+协程(不够的,可以看看之前写好的文章)
  • 熟悉paging2者,阅读体验更佳(没有也可尝试看看)

1、回顾Jetpack—Paging2

回顾一下上一篇所讲解的Paging2

是不是提到过三大核心类??

那么!Paging3的核心类呢?有哪些呢?

  • Paging3核心类之PagingDataAdapter(原PagedListAdpater)
  • Paging3核心类之PagingData(原PagedList)
  • Paging3核心类之PagingSource(原拥有三种格式的DataSource)

那么它们之间的改动就仅仅是命名上的改动么?

NO!NO!NO!要是真是如此,也不会单独另开一篇讲解了!

2、开讲Jetpack—Paging3

2.1 配置准备

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}
...略

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding = true 
    }
    dataBinding {
        enabled = true
    }

...略

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
    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 "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 'androidx.paging:paging-runtime:3.0.0-alpha03'
    implementation 'com.squareup.picasso:picasso:2.71828'
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'

这里可以看到,我这里同时开启了ViewBinding与DataBinding,然后引入了kotlin-kapt插件和一系列库。

网络权限以及允许Http不可少

    <uses-permission android:name="android.permission.INTERNET" />

    <application
		...略
        android:networkSecurityConfig="@xml/network_security_config">
        
        <activity android:name=".activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

这没啥说的

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

到这里准备工作做好了,现在依然按照上面顺序依次讲解Paging3!

2.2 PagingDataAdapter

class MovieAdapter(private val context: Context) :
    PagingDataAdapter<Movie, BindingViewHolder>(object : DiffUtil.ItemCallback<Movie>() {
        override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            // kotlin === 引用 , == 内容
            // Java == 引用, equals 内容
            return oldItem == newItem
        }

    }) {
    override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
        val movie = getItem(position)
        movie?.let {
        	//DataBinding相关逻辑,这里不做详解
            val binding = holder.binding as MovieItemBinding
            binding.movie = it
            binding.networkImage = it.cover
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
        val binding = MovieItemBinding.inflate(LayoutInflater.from(context), parent, false)
        return BindingViewHolder(binding)
    }
    
}

我们可以看到,除了Item改为DataBinding外,对应的Adapter和之前差不多,也需要实现对应的差分比较。而且逻辑和之前得到相似。

不过对应的BindingViewHolder为了等哈方便,因此单开了一个类(没有写成类部类)

class BindingViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)

结合ViewBinding,代码就这光溜溜的一句。

当然还有Json实体类

data class Movies(
    @SerializedName("subjects")
    val movieList: List<Movie>,
    @SerializedName("has_more")
    val hasMore: Boolean
)

这里准备用上一篇的4.2格式的返回结构!

总结

这个核心类感觉和之前Paging2的使用效果差不多。

那看看下一个!

2.3 PagingData

class MovieViewModel : ViewModel() {

    private val movies by lazy {
        Pager(
            config = PagingConfig(pageSize = 8, prefetchDistance = 8, initialLoadSize = 16),
            pagingSourceFactory = { MoviePagingSource() })
            .flow   //转为Flow流
            .cachedIn(viewModelScope) //使其生命周期与ViewModel相互绑定
    }

    fun loadMovie(): Flow<PagingData<Movie>> = movies
}

代码解析:

  • 我们这里可以看到在这通过懒加载lazy,实例化了Pager对象

    • pageSize 毋庸置疑,这里表示每页长度为多少
    • prefetchDistance 预取距离,表示必须离加载内容边缘多远才能触发进一步加载。一般设置为大于0,小于等于pageSize
    • initialLoadSize 来自PagingSource 的初始加载定义请求的加载大小,通常大于pageSize ,因此在第一次加载数据时,加载的内容范围足够大以覆盖小滚动。
  • 并将对应的Page对象转为了Flow流,然后通过cachedIn使其生命周期与ViewModel相互绑定

  • 最后通过loadMovie方法,将对应的Flow返回出去

这里我们看到,在初始化Pager对象时,使用了pagingSourceFactory = { MoviePagingSource() }指定了对应工厂为MoviePagingSource

因此

2.4 PagingSource

class MoviePagingSource : PagingSource<Int, Movie>() {

    //currentPage,pageSize
    //1,16
    //3,8
    //4,8

    //prevKey,nextKey
    //null,3
    //2,4
    //3,5

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
        delay(2000) //这里为了显示加载更多的效果所以特意挂起2秒
        val currentPage = params.key ?: 1
        val pageSize = params.loadSize
        val movies = RetrofitClient.createApi(MovieApi::class.java).getMovies(currentPage, pageSize)
        Log.d("hqk", "currentPage:$currentPage,pageSize:$pageSize")

        var prevKey: Int? = null
        var nextKey: Int? = null

        val realPageSize = 8
        val initialLoadSize = 16
        if (currentPage == 1) {
            prevKey = null
            nextKey = initialLoadSize / realPageSize + 1
        } else {
            prevKey = currentPage - 1
            nextKey = if (movies.hasMore) currentPage + 1 else null
        }
        Log.d("hqk", "prevKey:$prevKey,nextKey:$nextKey")

        return try {
            LoadResult.Page(
                data = movies.movieList,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            e.printStackTrace()
            return LoadResult.Error(e)
        }
    }
}

注意哟,这里重点来了!

  • 还记得上面创建PagingConfig这个对象时所传入的参数吧!

  • 第三个参数initialLoadSize = 16表示首次将会加载16条数据(这里官方做法也比pageSize大,最好是pageSize的n倍)

  • 那么在这里第一页加载也就是首次加载,也会加载16条数据,而我们设置的pageSize=8

  • 也就是说,首次加载第一页的时候,已经成功的加载了第一页和第二页数据,

  • 因此在加载下一页的时候,就应该从第三页开始加载

        //currentPage,pageSize  //分别表示:当前页数,以及对应页数加载的个数
        //1,16
        //3,8
        //4,8
    
        //prevKey,nextKey  //当前页数,下一页数,null表示首次加载第一页
        //null,3
        //2,4
        //3,5
    
  • 也就是说:从第三页开始,后面的每一页的个数才是对应pageSize所设置的条数

再次注意《重点!!!》

  • 这里一定要做对应处理,就算你没有设置initialLoadSize属性,官方默认在对应属性上扩大了3倍pageSize
  • 如果你没有处理prevKey,nextKey,那么你第二页和第三页加载的都是首次已经加载过的数据 !
  • 你将会在列表上看到有两页数据重复的bug!

来看看官方代码

    @JvmField
    @IntRange(from = 1)
     //这里默认乘以3
    val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,

接着,

2.5 来看看如何使用

class MainActivity : AppCompatActivity() {

    private val movieViewModel by viewModels<MovieViewModel>()

    private val mBinding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        val movieAdapter = MovieAdapter(this)

        mBinding.apply {
            recyclerView.adapter = movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
            swipeRefreshLayout.setOnRefreshListener {
                movieAdapter.refresh()
            }
        }

        lifecycleScope.launchWhenCreated {
            movieViewModel.loadMovie().collectLatest {
                movieAdapter.submitData(it)
            }
        }

        lifecycleScope.launchWhenCreated {
            movieAdapter.loadStateFlow.collectLatest { state ->
                mBinding.swipeRefreshLayout.isRefreshing = state.refresh is LoadState.Loading
            }
        }
    }
}

注意

  • 这里使用movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
  • 设置了对应Adapter对应的加载尾(加载更多)为MovieLoadMoreAdapter

最后来看看运行效果

在这里插入图片描述
哈哈哈哈,够具体吧,这样还不明白就来打我。坐标成都!!

Demo地址: 点我下载

结束语

好了,本篇到这里就结束了,相信看到这里的小伙伴已经对Jetpack-Paging3有了相当深刻的认知。下一篇将会结合Jetpack前面所讲解的知识点,整合成一个实战应用!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值