目录

Jetpack:Paging分页库之使用基础

引言

也许你还在RecyclerView的下拉加载而烦恼,也许你还在为做分页缓存而头疼,所以,他来了——Paging分页库。 我将代码存到了gitub上,点击PagingDemo跳转。强烈建议下载代码后,再对照博文阅读。

技巧
别盯着油管上谷歌官方2018年IO大会的教程了,Paging库中的代码早已发生了翻天覆地的变化。时代变了,大人~

配置

配置我使用versions的写法,请结合Demo查看,我提一下几个要点。

注解使用kapt

请确保你使用kapt来解析注解

1
2
3
4
5
apply plugin: 'kotlin-kapt'

dependencies {
    kapt deps.room.compiler
}

请确保注解解释器的版本大于2.3.0

1
versions.room = "2.3.0-alpha01"

否则你的注解将无法正确解析。

Kotlin/Java编译器版本

请确保在你的Kotlin/Java版本皆指向1.8。 在module的build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
    }
}
注意
如果你注意以上两点,那么你的程序基本不会因为配置的原因原地爆炸。

数据库

数据库包含实体层(Table)、Dao层(SQL)、数据库层(Scheme)。

Entity

为类打上@Entity注解,为主键id打上@PrimaryKey注解并标明自增。

1
2
3
4
5
6
@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val name: String
)

Dao

定义了三个方法

  • allUsers查询,我们的终极目的就是查出来
  • insert插入,后面用于填入模拟数据
  • deleteAll删除,会清空数据库中所有的数据

重点:

  • 我们的allUsers()返回PagingSource<K, V>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    fun allUsers(): PagingSource<Int, User>

    @Insert
    fun insert(users: List<User>)

    @Query("DELETE FROM User")
    fun deleteAll()
}

Database

为类打上Database注解并指明包含的类和数据库版本,引入了dao层,并且通过静态方法创建静态实例。

技巧
建议大家学会这种方法,不要再在Repositories层或者viewModel层build数据库了,太杂了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        private var instance: UserDatabase? = null

        /**
         * 提供互斥的静态方法用于获取数据库实例
         * 如果实例没创建,则创建;已经创建了,直接返回实例
         * 减少了创建实例所花费的开销
         */
        @Synchronized
        fun get(context: Context): UserDatabase {

            if (instance == null) {

                instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    "UserDatabase"
                ).build()
            }
            return instance!!
        }
    }
}

适配器

适配器完成三件事

  • Layout
  • ViewHolder
  • Adapter

layout

为Activity和RecyclerView的item分别建立布局。

Activity

注意RecyclerView要指定layoutManager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layoutManager="LinearLayoutManager"/>

    <Button
        android:id="@+id/delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

RecycleView的item

两个文本控件,分别用于显示User的id和name。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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="wrap_content">

    <TextView
        android:id="@+id/item_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/item_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/item_id"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

ViewHolder

根据我们创建的item布局和User实体类,我们写一个ViewHolder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class UserViewHolder(parent: ViewGroup): RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
) {

    // 控件
    private val idView = itemView.item_id
    private val nameView = itemView.item_name

    // 数据
    private var user: User? = null

    // 用于数据与控件的绑定
    fun bindTo(user : User?) {
        this.user = user
        idView.text = user?.id.toString()
        nameView.text = user?.name
    }
}

Adapter

重点:

  • 继承了PagingDataAdapter<Object, ViewHolder>
  • diffCallback的判断,建议采用这种变量写法会比较清晰(Kotlin的特性不用白不用)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * 继承PagingDataAdapter
 */
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(diffCallback) { // diffCallback用内部定义的变量
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        // 数据绑定:传入条目的位置信息,将数据绑定到这个位置的条目上
        holder.bindTo(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        // 布局创建:传入父布局,在父布局中创建布局
        return UserViewHolder(parent)
    }

    companion object {
        // 同异判断:分别用于判断条目是否更新和内容是否更新
        private 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
  • Activity

ViewModel

重点:

  • 我们的数据由Pager对象构建,PagingConfig作为参数传入,方法体中执行数据库查询操作,最后flow来获得Flow<PagingData<Value>>对象,最终将其赋值给allUsers
  • 我定义了两个挂起方法和一组模拟数据,我将在Activity中的Kotlin协程中使用它们。
  • 你发现我的模拟数据由id,但是上面的数据库中又指明id是自增的,这很矛盾。其实模拟数据的id只是我用来方便创建对象用的,真正的id由数据库生成,我们最终数据呈现所使用的id也是数据库中的id。但我这样做希望提醒你一点:我在insertAllmap中设置id=0,那么就会启用自增,数据库中的id,与你创建对象的id属性无关。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MainViewModel(app: Application): AndroidViewModel(app) {

    private val dao = UserDatabase.get(app).userDao()

    // 模拟数据,用于填充数据库
    private val USER_DATA = arrayListOf<User>(
        User(1, "Name1"),
        User(2, "Name2"),
        User(3, "Name3"),
        User(4, "Name4"),
        User(5, "Name5")
    )

    /**
     * 通过Pager的构造方法中的config参数配置分页的参数
     * 通过Pager的方法体传入数据
     */
    val allUsers = Pager(
        PagingConfig(
            pageSize = 5,
            enablePlaceholders = true
        )
    ) {
        dao.allUsers()
    }.flow

    // 删除所有数据
    suspend fun deleteAll() {
        withContext(Dispatchers.IO) {
            dao.deleteAll()
        }
    }

    // 填充模拟数据
    suspend fun insertAll(context: Context) {
        withContext(Dispatchers.IO) {
            UserDatabase.get(context.applicationContext).userDao()
                .insert(
                    USER_DATA.map { User(id = 0, name = it.name) }
                )
        }
    }
}

Activity

重点:

  • 在协程中,你需要adapter.submitData(it)来将数据传递给适配器。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
lass MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<MainViewModel>()

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

        val adapter = UserAdapter()
        recyclerview.adapter = adapter

        lifecycleScope.launch {
            // 插入所有模拟数据
            viewModel.insertAll(application)
            // 更新适配器数据
            @OptIn(ExperimentalCoroutinesApi::class)
            viewModel.allUsers.collectLatest {
                adapter.submitData(it)
            }
        }

        // 绑定删除数据事件
        delete.setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                viewModel.deleteAll()
            }
        }
    }
}

总结

这只是一个很基础的Demo。我们平时的开发场景,更多是结合通过网络来进行分页处理。在下一篇博文中,我将陈述如何操作。 我建议你更多的参考Google在Github上留下的Demo。虽然有些写法并不好,但核心的东西都会表达出来,不要再依赖过时的视频来进行学习。