美文网首页
Jetpack之Paging

Jetpack之Paging

作者: 0246eafe46bd | 来源:发表于2021-12-18 23:04 被阅读0次

Paging 3

Paging库的架构

Paging库组件在应用的三个层运行


paging3-library-architecture.png

Paging的关键组件

PagingSource

定义数据源,以及如何从该数据源检索数据,PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据

RemoteMediator

将来自网络的页面数据加载到数据库中,但不会直接将数据加载到界面中

Pager

Pager 基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中的 PagingData 实例

PagingData

PagingData 对象是用于存放分页数据快照的容器,它会查询 PagingSource 对象并存储结果

PagingDataAdapter

一种处理分页数据的 RecyclerView 适配器

Paging数据库分页实例

添加依赖

implementation "androidx.room:room-runtime:2.3.0"
// optional - Paging 3 Integration
kapt("androidx.room:room-compiler:2.3.0")
implementation "androidx.paging:paging-runtime:3.1.0"

实现数据库相关

数据库访问使用Room框架

创建实体类User

// 使用@Entity注解,将User声明成了一个实体类
@Entity
data class User(var name: String, var age: Int) {
    // 使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

创建UserDao

Dao必须使用接口,访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而Dao要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道

// 使用了一个@Dao注解,这样Room才能将它识别成一个Dao
@Dao
interface UserDao {
    @Insert
    fun insertUser(user: User): Long

    // 将方法中传入的参数指定到SQL语句
    @Query("select * from User where age > :age")
    fun loadAllUserOlderThan(age: Int): PagingSource<Int, User>
}

注意:上面的 loadAllUserOlderThan 方法返回值为 PagingSource<Int, User>,代表 Paging 的数据源,Int 代表传入数据类型,User 代码获取的数据类型。使用 Room 框架时,从数据库获取数据可以直接封装为 PagingSource,但从网络获取的就需要自己定义 PagingSource 了

定义Database

定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例

// 使用@Database注解,在注解中声明数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        private var instance: AppDatabase? = null

        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            return Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            ).build().apply {
                instance = this
            }
        }
    }
}

创建仓库类

object Repository {
    private const val PAGE_SIZE = 5
    val userDao = AppDatabase.getDatabase(MyApp.context).userDao()

    fun query(minAge: Int) = Pager(config = PagingConfig(PAGE_SIZE)) {
        userDao.loadAllUserOlderThan(minAge)
    }.flow
}

实现RecyclerView的Adapter

RecyclerView的Adapter必须继承自PagingDataAdapter并提供一个DiffUtil.ItemCallback对象,Paging 3在内部会使用DiffUtil来管理数据变化,不需要传递数据源给Adapter,因为数据源是由Paging 3在内部自己管理

class UserAdapter : PagingDataAdapter<User, UserAdapter.ViewHolder>(diffCallback) {

    inner class ViewHolder(private val binding: PagingRecyclerviewItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 绑定界面数据
         * @param user User
         */
        fun bind(user: User) {
            binding.userName.text = user.name
            binding.userAge.text = user.age.toString()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingRecyclerviewItemBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_recyclerview_item,
            parent,
            false
        )
        val holder = ViewHolder(binding)
        binding.root.setOnClickListener {
            val pos = holder.bindingAdapterPosition
            "你点击了第${pos}个数据,信息为${getItem(pos)}".showToast(MyApp.context)
        }
        return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        item?.let { holder.bind(it) }
    }


    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem == newItem
            }
        }
    }
}

实现ViewModel

class PagingViewModel : ViewModel() {
    /**
     * Paging查询
     */
    fun queryPaging(minAge: Int): Flow<PagingData<User>> {
        return Repository.query(minAge).cachedIn(viewModelScope)
    }

    /**
     * 插入数据
     * @param user User
     */
    fun insert(user: User) {
        viewModelScope.launch {
            Repository.insert(user)
        }
    }
}

Activity中使用

class PagingActivity : AppCompatActivity() {
    private lateinit var userAdapter: UserAdapter
    private val pagingViewModel by lazy {
        ViewModelProvider(this, PagingViewModelFactory()).get(PagingViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        userAdapter = UserAdapter()
        paging_recyclerview.adapter = userAdapter
        paging_recyclerview.layoutManager = LinearLayoutManager(this)

        // 用于给数据库插入一些数据
        insert_btn.setOnClickListener {
            val name = user_name.text.toString()
            val age = user_age.text.toString().toInt()
            pagingViewModel.insert(User(name, age))
        }

        val minAge = min_age.text.toString().toInt()
        lifecycleScope.launch {
            pagingViewModel.queryPaging(minAge).collect {
                LogUtil.d(javaClass.name, "onCreate: ${it.toString()}")
                userAdapter.submitData(it)
            }
        }
    }
}

Paging 网络请求分页实例

以获取笑话为例,请求数据使用 retrofit

创建实体类 Joke

data class Joke(
    val code: Int,
    val message: String,
    val result: List<Result>
) {
    data class Result(
        val comment: String,
        val down: String,
        val forward: String,
        val header: String,
        val images: Any,
        val name: String,
        val passtime: String,
        val sid: String,
        val text: String,
        val thumbnail: String,
        val top_comments_content: String,
        val top_comments_header: String,
        val top_comments_name: String,
        val top_comments_uid: String,
        val top_comments_voiceuri: String,
        val type: String,
        val uid: String,
        val up: String,
        val video: String
    )
}

创建网络请求接口

interface JokeService {
    @GET("getJoke")
    fun getJokes(@QueryMap params: Map<String, String>): Call<Joke>
}

创建数据源

使用 PagingSource<Key, Value> 类即可通过 Kotlin 协程进行异步加载,Key 定义了用于加载数据的标识符,也就是请求数据时的参数类型。例如,将 Int 页码传递给 Retrofit 来从网络加载各页 User 对象,应选择 Int 作为 Key 类型,选择 User 作为 Value 类型

// PagingSource的泛型,第一个表示页数的数据类型,一般是整型;第二个表示每一项的数据类型
class JokePagingSource(private val jokeService: JokeService) : PagingSource<Int, Joke.Result>() {
    override fun getRefreshKey(state: PagingState<Int, Joke.Result>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Joke.Result> {
        // 当前页数
        val curPage = params.key ?: 1
        // 每一页包含多少条数据
        val pageSize = params.loadSize
        // 获取当前页的数据,这里需要try一下,因为你第一次打开的时候就可能没网,
        // 因此要在catch块中返回LoadResult.Error,否则程序这里就挂掉了,
        // Adapter.addLoadStateListener 添加的监听,也不会有LoadState.Error状态
        try {
            val jokes = jokeService.getJokes(
                hashMapOf(
                    "page" to curPage.toString(),
                    "count" to pageSize.toString(),
                    "type" to "video"
                )
            ).await().result

            // 前一页和下一页
            val prevPage = if (curPage > 1) curPage - 1 else null
            var nextPage = if (jokes.isNullOrEmpty()) null else curPage + 1

            // 构建LoadResult.Page对象并返回,第一个参数是通过网络获取的列表数据
            return LoadResult.Page(jokes, prevPage, nextPage)
        } catch (e: Exception) {
            LogUtil.d(javaClass.name, "load: ${e.message}")
            return LoadResult.Error(e)
        }
    }
}

仓库类

object Repository {

    private const val PAGE_SIZE = 5
    private val jokeService = ServiceCreator.create<JokeService>()

    /**
     * 获取笑话
     * @return Flow<PagingData<Result>>
     */
    fun getJokes(): Flow<PagingData<Joke.Result>> = Pager(
        config = PagingConfig(PAGE_SIZE), pagingSourceFactory = { JokePagingSource(jokeService) }
    ).flow
}

ViewModel

class PagingViewModel : ViewModel() {
    /**
     * 请求笑话
     * @return Flow<PagingData<Joke.Result>>
     */
    fun getJokes(): Flow<PagingData<Joke.Result>> {
        // cachedIn()用于将数据在viewModelScope这个作用域内进行缓存,这样旋转手机就会不再发出网络请求
        return Repository.getJokes().cachedIn(viewModelScope)
    }
}

实现RecyclerView的Adapter

我这里为了简单,直接复用的前面 “Paging数据库分页实例” 的同一个布局,也只是显示了请求的两个文本内容

class JokeAdapter : PagingDataAdapter<Joke.Result, JokeAdapter.ViewHolder>(diffCallback) {

    inner class ViewHolder(private val binding: PagingRecyclerviewItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 绑定界面数据
         * @param joke Joke.Result
         */
        fun bind(joke: Joke.Result) {
            binding.userName.text = joke.name
            binding.userAge.text = joke.text
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingRecyclerviewItemBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_recyclerview_item,
            parent,
            false
        )
        val holder = ViewHolder(binding)
        binding.root.setOnClickListener {
            val pos = holder.bindingAdapterPosition
            "你点击了第${pos}个数据,信息为${getItem(pos)}".showToast(MyApp.context)
        }
        return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        item?.let { holder.bind(it) }
    }


    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<Joke.Result>() {
            override fun areItemsTheSame(oldItem: Joke.Result, newItem: Joke.Result): Boolean {
                return oldItem.sid == newItem.sid
            }

            override fun areContentsTheSame(oldItem: Joke.Result, newItem: Joke.Result): Boolean {
                return oldItem == newItem
            }
        }
    }
}

底部适配器

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {
    inner class ViewHolder(private val binding: PagingFooterBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮
         * @param loadState LoadState
         */
        fun bind(loadState: LoadState) {
            binding.footerProgressBar.isVisible = loadState is LoadState.Loading
            binding.footerRetry.isVisible = loadState is LoadState.Error
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingFooterBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_footer,
            parent,
            false
        )
        // 点击重试
        binding.footerRetry.setOnClickListener {
            retry()
        }
        return ViewHolder(binding)
    }
}

Activity中使用

class PagingActivity : AppCompatActivity() {
    private val pagingViewModel by lazy {
        ViewModelProvider(this, PagingViewModelFactory()).get(PagingViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        // 下面是使用Paging进行网络请求笑话的
        val jokeAdapter = JokeAdapter()
        // withLoadStateFooter添加重试,FooterAdapter布局的高度记得不要设为match_parent,否则加载失败后,还可以滑动
        paging_recyclerview.adapter =
            jokeAdapter.withLoadStateFooter(FooterAdapter { jokeAdapter.retry() })
        paging_recyclerview.layoutManager = LinearLayoutManager(this)

        lifecycleScope.launch {
            pagingViewModel.getJokes().collect {
                jokeAdapter.submitData(it)
            }
        }

        /**
         * 监听加载状态,控制的是第一次加载数据的状态,之后滑到下面加载失败,是由FooterAdapter控制的
         */
        jokeAdapter.addLoadStateListener {
            it.refresh.let { loadState ->
                progress_bar.isVisible = loadState is LoadState.Loading
                retry.isVisible = loadState is LoadState.Error
            }
        }
    }
}

相关文章

网友评论

      本文标题:Jetpack之Paging

      本文链接:https://www.haomeiwen.com/subject/ctrafrtx.html