В этом руководстве я покажу вам, как использовать библиотеку подкачки из компонентов архитектуры Android с базой данных на основе Room в приложении для Android.
Вы узнаете, как использовать библиотеку подкачки для эффективной загрузки больших наборов данных из базы данных с поддержкой Room, предоставляя своим пользователям более плавную работу при прокрутке в RecyclerView.
Предпосылки
Чтобы следовать этому руководству, вам понадобится:
- Android Studio 3.1.3 или выше
- Плагин Kotlin 1.2.51 или выше
- базовое понимание компонентов архитектуры Android (особенно базы данных
LiveData
и Room )
Если вы еще не узнали о компонентах архитектуры, настоятельно рекомендуем ознакомиться с нашей удивительной серией статей о компонентах архитектуры Android от Tin Megali. Убедитесь, что вы погрузитесь!
Пример проекта для этого урока можно найти в нашем репозитории GitHub, чтобы вы могли легко следить за ним.
Что такое пейджинговая библиотека?
Библиотека подкачки — это еще одна библиотека, добавленная к компонентам архитектуры . Библиотека помогает эффективно управлять загрузкой и отображением большого набора данных в RecyclerView
. Согласно официальным документам:
Библиотека подкачки упрощает постепенную и плавную загрузку данных в
RecyclerView
вашего приложения.
Если какая-либо часть вашего приложения Android будет отображать большой набор данных из локального или удаленного источника данных, но отображать только его часть за раз, то вам следует рассмотреть возможность использования библиотеки подкачки. Это поможет улучшить производительность вашего приложения!
Так зачем использовать библиотеку подкачки?
Теперь, когда вы увидели введение в библиотеку подкачки, вы можете спросить, зачем ее использовать? Вот несколько причин, по которым вам следует рассмотреть возможность его использования при загрузке больших наборов данных в RecyclerView
.
- Он не запрашивает данные, которые не нужны. Эта библиотека запрашивает только те данные, которые видны пользователю — когда пользователь прокручивает список.
- Экономит батарею пользователя и потребляет меньше пропускной способности. Поскольку он запрашивает только те данные, которые необходимы, это экономит некоторые ресурсы устройства.
Это не будет эффективно при работе с большим объемом данных, так как базовый источник данных извлекает все данные, даже если пользователю будет отображаться только подмножество этих данных. В такой ситуации мы должны рассмотреть подкачку данных.
1. Создайте проект Android Studio
MainActivity
Android Studio 3 и создайте новый проект с пустым действием MainActivity
. Обязательно установите флажок Включить поддержку Kotlin .
2. Добавьте компоненты архитектуры
После создания нового проекта добавьте следующие зависимости в ваш build.gradle . В этом уроке мы используем последнюю версию библиотеки подкачки версии 1.0.1, а Room — 1.1.1 (на момент написания статьи).
1
2
3
4
5
6
7
|
dependencies {
implementation fileTree(dir: ‘libs’, include: [‘*.jar’])
implementation «android.arch.persistence.room:runtime:1.1.1»
kapt «android.arch.persistence.room:compiler:1.1.1»
implementation «android.arch.paging:runtime:1.0.1»
implementation «com.android.support:recyclerview-v7:27.1.1»
}
|
Эти артефакты доступны в репозитории Google Maven.
1
2
3
4
5
6
|
allprojects {
repositories {
google()
jcenter()
}
}
|
Добавляя зависимости, мы научили Gradle, как найти библиотеку. Убедитесь, что вы не забыли синхронизировать свой проект после добавления их.
3. Создайте сущность
Создайте новый класс данных Kotlin Person
. Для простоты наша сущность Person
имеет только два поля:
- уникальный идентификатор (
id
) - имя человека (
name
)
Кроме того, toString(
метод toString(
который просто возвращает name
.
01
02
03
04
05
06
07
08
09
10
|
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
@Entity(tableName = «persons»)
data class Person(
@PrimaryKey val id: String,
val name: String
) {
override fun toString() = name
}
|
4. Создайте DAO
Как вы знаете, для доступа к данным нашего приложения из библиотеки Room нам нужны объекты доступа к данным (DAO). В нашем собственном случае мы создали PersonDao
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import android.arch.lifecycle.LiveData
import android.arch.paging.DataSource
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
@Dao
interface PersonDao {
@Query(«SELECT * FROM persons»)
fun getAll(): LiveData<List<Person>>
@Query(«SELECT * FROM persons»)
fun getAllPaged(): DataSource.Factory<Int, Person>
@Insert
fun insertAll(persons: List<Person>)
@Delete
fun delete(person: Person)
}
|
В нашем классе PersonDao
у нас есть два метода @Query
. Одним из них является getAll()
, который возвращает LiveData
который содержит список объектов Person
. Другой является getAllPaged()
, который возвращает DataSource.Factory
.
Согласно официальным документам, класс DataSource
является:
Базовый класс для загрузки страниц данных снимка в
PagedList
.
PagedList
— это особый вид List
для отображения выгружаемых данных в Android:
PagedList
— это список, который загружает свои данные кусками (страницами) из источника данных. Доступ кloadAround(int)
можно получить с помощьюget(int)
, а дальнейшая загрузка может быть вызвана с помощьюloadAround(int)
.
Мы вызвали статический метод Factory
в классе DataSource
, который служит фабрикой (для создания объектов без указания точного класса создаваемого объекта) для DataSource
. Этот статический метод принимает два типа данных:
- Ключ, который идентифицирует элементы в
DataSource
. Обратите внимание, что для запроса Room страницы нумеруются, поэтому мы используемInteger
в качестве типа идентификатора страницы. Можно использовать страницы с ключами, используя библиотеку подкачки, но в настоящее время Room этого не предлагает. - Тип элементов или объектов (POJO) в списке, загруженном источником данных s.
5. Создайте базу данных
Вот как выглядит наш класс базы данных Room AppDatabase
:
01
02
03
04
05
06
07
08
09
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
|
import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.content.Context
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.chikeandroid.pagingtutsplus.utils.DATABASE_NAME
import com.chikeandroid.pagingtutsplus.workers.SeedDatabaseWorker
@Database(entities = [Person::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun personDao(): PersonDao
companion object {
// For Singleton instantiation
@Volatile private var instance: AppDatabase?
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance
?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance()?.enqueue(request)
}
})
.build()
}
}
}
|
Здесь мы создали один экземпляр нашей базы данных и предварительно заполнили его данными, используя новый API WorkManager . Обратите внимание, что предварительно заполненные данные — это просто список из 1000 имен (чтобы узнать больше, ознакомьтесь с примером исходного кода).
6. Создание ViewModel
Чтобы наш пользовательский интерфейс мог хранить, наблюдать и обслуживать данные с учетом жизненного цикла, нам нужна ViewModel
. Наша PersonsViewModel
, которая расширяет класс AndroidViewModel
, будет функционировать как наша ViewModel
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import android.arch.paging.DataSource
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.chikeandroid.pagingtutsplus.data.AppDatabase
import com.chikeandroid.pagingtutsplus.data.Person
class PersonsViewModel constructor(application: Application)
: AndroidViewModel(application) {
private var personsLiveData: LiveData<PagedList<Person>>
init {
val factory: DataSource.Factory<Int, Person> =
AppDatabase.getInstance(getApplication()).personDao().getAllPaged()
val pagedListBuilder: LivePagedListBuilder<Int, Person> = LivePagedListBuilder<Int, Person>(factory,
50)
personsLiveData = pagedListBuilder.build()
}
fun getPersonsLiveData() = personsLiveData
}
|
В этом классе у нас есть одно поле с именем personsLiveData
. Это поле просто LiveData
которое содержит объекты PagedList
of Person
. Поскольку это LiveData
, наш пользовательский интерфейс ( Activity
или Fragment
) будет наблюдать эти данные, вызывая метод getter getPersonsLiveData()
.
Мы инициализировали personsLiveData
внутри блока init
. Внутри этого блока мы получаем DataSource.Factory
, вызывая синглтон AppDatabase
для объекта PersonDao
. Когда мы получаем этот объект, мы вызываем getAllPaged()
.
Затем мы создаем LivePagedListBuilder
. Вот что говорится в официальной документации о LivePagedListBuilder
:
LiveData<PagedList>
дляLiveData<PagedList>
, с даннымиDataSource.Factory
иPagedList.Config
.
Мы предоставляем его конструктору DataSource.Factory
в качестве первого аргумента и размер страницы в качестве второго аргумента (в нашем случае размер страницы будет равен 50). Как правило, вы должны выбрать размер, превышающий максимальный номер, который вы могли бы показать пользователю сразу. В конце мы вызываем build()
чтобы build()
и вернуть нам LiveData<PagedList>
.
7. Создание PagedListAdapter
Чтобы показать наши данные PagedList
в RecyclerView
, нам нужен PagedListAdapter
. Вот четкое определение этого класса из официальных документов:
Базовый класс
RecyclerView.Adapter
для представления выгружаемых данных изPagedList
вRecyclerView
.
Поэтому мы создаем PersonAdapter
который расширяет PagedListAdapter
.
01
02
03
04
05
06
07
08
09
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
|
import android.arch.paging.PagedListAdapter
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.chikeandroid.pagingtutsplus.R
import com.chikeandroid.pagingtutsplus.data.Person
import kotlinx.android.synthetic.main.item_person.view.*
class PersonAdapter(val context: Context) : PagedListAdapter<Person, PersonAdapter.PersonViewHolder>(PersonDiffCallback()) {
override fun onBindViewHolder(holderPerson: PersonViewHolder, position: Int) {
var person = getItem(position)
if (person == null) {
holderPerson.clear()
} else {
holderPerson.bind(person)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
return PersonViewHolder(LayoutInflater.from(context).inflate(R.layout.item_person,
parent, false))
}
class PersonViewHolder (view: View) : RecyclerView.ViewHolder(view) {
var tvName: TextView = view.name
fun bind(person: Person) {
tvName.text = person.name
}
fun clear() {
tvName.text = null
}
}
}
|
PagedListAdapter
используется так же, как и любой другой подкласс RecyclerView.Adapter
. Другими словами, вы должны реализовать методы onCreateViewHolder()
и onBindViewHolder()
.
Чтобы расширить абстрактный класс PagedListAdapter
, вы должны будете указать в его конструкторе тип PageLists
(это должен быть простой старый класс Java: POJO), а также класс, расширяющий ViewHolder
который будет использоваться адаптером. В нашем случае мы дали ему Person
и PersonViewHolder
в качестве первого и второго аргумента соответственно.
Обратите внимание, что PagedListAdapter
требует, чтобы вы DiffUtil.ItemCallback
ему PageListAdapter
конструктору PageListAdapter
. DiffUtil
— это служебный класс RecyclerView
который может вычислить разницу между двумя списками и вывести список операций обновления, которые преобразуют первый список во второй. ItemCallback
— это внутренний абстрактный статический класс (внутри DiffUtil
), используемый для вычисления DiffUtil
между двумя ненулевыми элементами в списке.
В частности, мы предоставляем PersonDiffCallback
нашему конструктору PagedListAdapter
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
import android.support.v7.util.DiffUtil
import com.chikeandroid.pagingtutsplus.data.Person
class PersonDiffCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Person?, newItem: Person?): Boolean {
return oldItem == newItem
}
}
|
Поскольку мы реализуем DiffUtil.ItemCallback
, мы должны реализовать два метода: areItemsTheSame()
и areContentsTheSame()
.
-
areItemsTheSame
вызывается для проверки, представляют ли два объекта один и тот же элемент. Например, если ваши элементы имеют уникальные идентификаторы, этот метод должен проверить их равенство идентификаторов. Этот метод возвращаетtrue
если два элемента представляют один и тот же объект, илиfalse
если они разные. -
areContentsTheSame
вызывается для проверки, имеют ли два элемента одинаковые данные. Этот метод возвращаетtrue
если содержимое элементов одинаковое, илиfalse
если они разные.
Наш внутренний класс PersonViewHolder
— это просто типичный RecyclerView.ViewHolder
. Он отвечает за связывание данных по мере необходимости из нашей модели в виджеты для строки в нашем списке.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class PersonAdapter(val context: Context) : PagedListAdapter<Person, PersonAdapter.PersonViewHolder>(PersonDiffCallback()) {
// …
class PersonViewHolder (view: View) : RecyclerView.ViewHolder(view) {
var tvName: TextView = view.name
fun bind(person: Person) {
tvName.text = person.name
}
fun clear() {
tvName.text = null
}
}
}
|
8. Отображение результата
В нашем onCreate()
нашего MainActivity
мы просто сделали следующее:
- инициализировать наше поле
viewModel
используя служебный классViewModelProviders
- создать экземпляр
PersonAdapter
- настроить наш
RecyclerView
- привязать
PersonAdapter
кRecyclerView
- наблюдать за
LiveData
и передавать объектыPagedList
вPersonAdapter
, вызываяsubmitList()
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.RecyclerView
import com.chikeandroid.pagingtutsplus.adapter.PersonAdapter
import com.chikeandroid.pagingtutsplus.viewmodels.PersonsViewModel
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: PersonsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(PersonsViewModel::class.java)
val adapter = PersonAdapter(this)
findViewById<RecyclerView>(R.id.name_list).adapter = adapter
subscribeUi(adapter)
}
private fun subscribeUi(adapter: PersonAdapter) {
viewModel.getPersonLiveData().observe(this, Observer { names ->
if (names != null) adapter.submitList(names)
})
}
}
|
Наконец, когда вы запускаете приложение, вот результат:
Во время прокрутки Room может предотвратить пробелы, загружая одновременно 50 элементов и делая их доступными для нашего PersonAdapter
, который является подклассом PagingListAdapter
. Но обратите внимание, что не все источники данных будут загружены быстро. Скорость загрузки также зависит от вычислительной мощности устройства Android.
9. Интеграция с RxJava
Если вы используете или хотите использовать RxJava в своем проекте, библиотека подкачки содержит еще один полезный артефакт: RxPagedListBuilder
. Вы используете этот артефакт вместо LivePagedListBuilder
для поддержки RxJava.
Вы просто создаете экземпляр RxPagedListBuilder
, предоставляя те же аргументы, что и для LivePagedListBuilder
— DataSource.Factory
и размер страницы. Затем вы вызываете buildObservable()
или buildFlowable()
чтобы вернуть Observable
или Flowable
для вашего PagedList
соответственно.
Чтобы явно предоставить Scheduler
для работы по загрузке данных, вы вызываете метод setFetchScheduler()
. Чтобы также предоставить Scheduler
для доставки результата (например, AndroidSchedulers.mainThread()
), просто вызовите setNotifyScheduler()
. По умолчанию setNotifyScheduler()
умолчанию используется для потока пользовательского интерфейса, а setFetchScheduler()
для пула потоков ввода-вывода.
Вывод
Из этого руководства вы узнали, как легко использовать компонент Paging из компонентов архитектуры Android (которые входят в состав Android Jetpack ) с Room. Это помогает нам эффективно загружать большие наборы данных из локальной базы данных, чтобы обеспечить более плавное взаимодействие с пользователем при прокрутке списка в RecyclerView
.
Я настоятельно рекомендую ознакомиться с официальной документацией, чтобы узнать больше о библиотеке подкачки в Android.