王学岗协程(十一)————Flow与Jetpack Paging3

代码讲解

Flow在很多地方都与Paging3结合使用,而且Paging3与Paging2也有很大的不同。所以这里讲解下。主要有以下内容
Paging3的结构组成
Flow与Paging3
下拉刷新
上拉加载更多与离奇bug的解决
上游数据缓存

在这里插入图片描述

数据从PagingSource来,Pager里设置PageConfig,加载完后会得到Flow,最后交给PagingDataAdapter更新UI
我们看下接口返回的数据
在这里插入图片描述
根据接口返回的数据我们设计两个实体类。

package com.dongnaoedu.jetpackpaging.model

import android.icu.text.CaseMap

data class Movie(val id: Int, val title: String, val rate: String, val cover: String)
package com.dongnaoedu.jetpackpaging.model

import com.google.gson.annotations.SerializedName

/**
 *  服务器响应的结果,是一个Json数据
 */
data class Movies(
    @SerializedName("subjects")
    val movieList: List<Movie>,
    @SerializedName("has_more")
    val hasMore: Boolean//是否还有下一页
)

我们在看下服务器要求的字段
在这里插入图片描述
根据服务器要求的我们设计我们访问网络的接口

package com.dongnaoedu.jetpackpaging.net

import com.dongnaoedu.jetpackpaging.model.Movies
import retrofit2.http.GET
import retrofit2.http.Query

interface MovieApi {
    //返回是个Movies对象,Movies对象里面有集合
    @GET("pkds.do")
    suspend fun getMovies(
        @Query("page") page: Int,
        @Query("pagesize") pageSize: Int
    ): Movies

}

代码结构

先看下目录结构
在这里插入图片描述

Activity代码

package com.dongnaoedu.jetpackpaging.activity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import com.dongnaoedu.jetpackpaging.adapter.MovieAdapter
import com.dongnaoedu.jetpackpaging.adapter.MovieLoadMoreAdapter
import com.dongnaoedu.jetpackpaging.databinding.ActivityMainBinding
import com.dongnaoedu.jetpackpaging.viewmodel.MovieViewModel
import kotlinx.coroutines.flow.collectLatest

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 {
            //withLoadStateFooter加载更多脚布局,需要一个适配器做参数
            //context要这么写this@MainActivity,单单写个this指的是mBinding对象
            recyclerView.adapter = movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
            swipeRefreshLayout.setOnRefreshListener {
                //刷新就重新获取数据
                movieAdapter.refresh()
            }
        }

        lifecycleScope.launchWhenCreated {
            //因为不断的刷新,有可能有多个值,我们这里只取最新的数据
            movieViewModel.loadMovie().collectLatest {
                movieAdapter.submitData(it)
            }
        }

        lifecycleScope.launchWhenCreated {
            //监听刷新。数据拿到了就停止刷新。loadStateFlow可以获取状态
            movieAdapter.loadStateFlow.collectLatest { state ->
                //判断state.refreh是否是LoadState.Loading
                mBinding.swipeRefreshLayout.isRefreshing = state.refresh is LoadState.Loading
            }
        }
    }
}

Adapter代码之BindingViewHolder

package com.dongnaoedu.jetpackpaging.adapter

import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding

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

Adapter之ImageViewBindingAdapter

package com.dongnaoedu.jetpackpaging.adapter

import android.graphics.Color
import android.text.TextUtils
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.dongnaoedu.jetpackpaging.R
import com.squareup.picasso.Picasso

/**
 * 自定义图片加载
 */
class ImageViewBindingAdapter {
    //必须是静态方法才能完成绑定,java调用Kotlin静态方法
    //需要两步,第一步使用companion object,第二步使用@JvmStatic注解
    companion object {
        @JvmStatic
        @BindingAdapter("image")
        fun setImage(imageView: ImageView, url: String) {
            if (!TextUtils.isEmpty(url)) {
                Picasso.get().load(url).placeholder(R.drawable.ic_launcher_background)
                    .into(imageView)
            } else {
                imageView.setBackgroundColor(Color.GRAY)
            }
        }
    }

}

Adapter之MovieAdapter

package com.dongnaoedu.jetpackpaging.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.dongnaoedu.jetpackpaging.databinding.MovieItemBinding
import com.dongnaoedu.jetpackpaging.model.Movie

//继承的是PagingDataAdapter,自定义一个BindingViewHolder
//PagingDataAdapter的构造方法有三个参数,mainDispatcher ,workerDispatcher和DiffUtil.ItemCallback
//前两个参数(调度器)是默认实现的,第三个参数需要我们自己实现,我们这里使用对象表达式
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//ID相同我们就认为是同一个Item
        }

        override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            //Movie是的data数据类,会重写比较内容的方法。比较的是Movie的属性值是否都相等
            // kotlin  === 引用 , == 内容
            // Java == 引用, equals 内容
            return oldItem == newItem
        }

    }) {
    override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
        val movie = getItem(position)
        movie?.let {
            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)
    }
}

Adapter之MovieLoadMoreAdapter

package com.dongnaoedu.jetpackpaging.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import com.dongnaoedu.jetpackpaging.databinding.MovieLoadmoreBinding

/**
 *
 * @author ningchuanqi
 * @version V1.0
 */
class MovieLoadMoreAdapter(private val context: Context) : LoadStateAdapter<BindingViewHolder>() {

    override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState) {
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): BindingViewHolder {
        val binding = MovieLoadmoreBinding.inflate(LayoutInflater.from(context), parent, false)
        return BindingViewHolder(binding)
    }
}

model之Movie代码

package com.dongnaoedu.jetpackpaging.model

import android.icu.text.CaseMap

/**
 *
 */
data class Movie(val id: Int, val title: String, val rate: String, val cover: String)

model代码之Movies

package com.dongnaoedu.jetpackpaging.model

import com.google.gson.annotations.SerializedName

/**
 *  服务器响应的结果,是一个Json数据
 */
data class Movies(
    @SerializedName("subjects")
    val movieList: List<Movie>,
    @SerializedName("has_more")
    val hasMore: Boolean//是否还有下一页
)

net代码之MovieApi

package com.dongnaoedu.jetpackpaging.net

import com.dongnaoedu.jetpackpaging.model.Movies
import retrofit2.http.GET
import retrofit2.http.Query

interface MovieApi {
    //返回是个Movies对象,Movies对象里面有集合
    @GET("pkds.do")
    suspend fun getMovies(
        @Query("page") page: Int,
        @Query("pagesize") pageSize: Int
    ): Movies

}

net代码之RetrofitClient

package com.dongnaoedu.jetpackpaging.net

import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

/**
 *
 * @author ningchuanqi
 * @version V1.0
 */
object RetrofitClient {

    private val instance: Retrofit by lazy {
        val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
            //Log.d("ning", it)
        })
        interceptor.level = HttpLoggingInterceptor.Level.BODY
        Retrofit.Builder()
            .client(OkHttpClient.Builder().addInterceptor(interceptor).build())
            .baseUrl("http://192.168.1.4:8080/pagingserver_war/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    fun <T> createApi(clazz: Class<T>): T {
        return instance.create(clazz) as T
    }
}

paging代码之MoviePagingSource

package com.dongnaoedu.jetpackpaging.paging

import android.util.Log
import androidx.paging.PagingSource
import com.dongnaoedu.jetpackpaging.model.Movie
import com.dongnaoedu.jetpackpaging.net.MovieApi
import com.dongnaoedu.jetpackpaging.net.RetrofitClient
import kotlinx.coroutines.delay

/**
 *
 * @author ningchuanqi
 * @version V1.0
 */
class MoviePagingSource : PagingSource<Int, Movie>() {

     //load就是要请求我的服务器,返回的是LoadResult<Int, Movie>对象
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
        delay(2000)//如果看不到加载更多的脚布局,可以增加这句代码
         //如果params.key为空,就是第一次加载,我们让currentPage等于1
        val currentPage = params.key ?: 1
         //一页有多少数据
        val pageSize = params.loadSize
        //在这里请求服务器,传入当前的page,和pageSize。
        val movies = RetrofitClient.createApi(MovieApi::class.java).getMovies(currentPage, pageSize)
        Log.d("ning", "currentPage:$currentPage,pageSize:$pageSize")

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

        val realPageSize = 8//每页的数据
        val initialLoadSize = 16
        if (currentPage == 1) {
            prevKey = null
            //nextKey为一次性加载的页数+1。比如我们一次性加载两页,那么下一页就是2+1
            nextKey = initialLoadSize / realPageSize + 1
        } else {
            prevKey = currentPage - 1
            //如果有下一页就+1,否则为null。
            nextKey = if (movies.hasMore) currentPage + 1 else null
        }
        Log.d("ning", "prevKey:$prevKey,nextKey:$nextKey")

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

viewmodel代码之MovieViewModel

package com.dongnaoedu.jetpackpaging.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.dongnaoedu.jetpackpaging.model.Movie
import com.dongnaoedu.jetpackpaging.paging.MoviePagingSource
import kotlinx.coroutines.flow.Flow

class MovieViewModel : ViewModel() {
//注意,viewModel的临时数据要放在属性上保存。如果不放到属性上保存
    //每次切换横竖屏都会重新加载。所以这里一定要这么写(A和B都不能忘记)。

    private val movies by lazy {//A处声明一个成员变量
//        Pager有一个flow对象
        Pager(
            config = PagingConfig(
                pageSize = 8,
                //距离最后一个Item多远时候加载数据,默认为PageSize,一定要大于一。太小的话会有Bug.给LoadMore预留足够的位置
                prefetchDistance = 8,
                initialLoadSize = 16//默认是3 X pagesize
            ),
            pagingSourceFactory = {MoviePagingSource()}
            //流的数据要缓存必须使用cachedIn
        //流会一直是活跃的只要我们给定的作用域是活跃的,activity没有挂掉,viewmodeScope就会一直在
        //The flow is kept active as long as the given [scope] is active
        ).flow.cachedIn(viewModelScope)//B处要用cachedIn缓存
    }
    //在ViewModel中请求,返回类型是Flow<PagingData<Movie>>
    fun loadMovie() : Flow<PagingData<Movie>> = movies

}

布局代码之activity_main

<?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/swipeRefreshLayout"
        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="wrap_content"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

布局代码之movie_item

<?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">

    <data>
        <variable
            name="networkImage"
            type="String" />
        <variable
            name="movie"
            type="com.dongnaoedu.jetpackpaging.model.Movie" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingVertical="10dip">


        <ImageView
            app:image="@{networkImage}"
            android:id="@+id/imageView"
            android:layout_width="100dip"
            android:layout_height="100dip"
            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: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="泰坦尼克号"
            android:text="@{movie.title}"/>

        <TextView
            android:id="@+id/textViewRate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="@+id/guideline"
            app:layout_constraintTop_toBottomOf="@+id/textViewTitle"
            tools:text="评分:8.9分"
            android:text="@{movie.rate}"/>

        <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>
</layout>

布局代码之movie_loadmore

<?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"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:padding="10dp">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="20dp"
        android:layout_height="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/tv_loading"/>

    <TextView
        android:id="@+id/tv_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="正在加载数据... ..."
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值