Статьи

Компоненты архитектуры Android: использование библиотеки подкачки вместе с комнатой

В этом руководстве я покажу вам, как использовать библиотеку подкачки из компонентов архитектуры Android с базой данных на основе Room в приложении для Android.

Вы узнаете, как использовать библиотеку подкачки для эффективной загрузки больших наборов данных из базы данных с поддержкой Room, предоставляя своим пользователям более плавную работу при прокрутке в RecyclerView.

Чтобы следовать этому руководству, вам понадобится:

Если вы еще не узнали о компонентах архитектуры, настоятельно рекомендуем ознакомиться с нашей удивительной серией статей о компонентах архитектуры Android от Tin Megali. Убедитесь, что вы погрузитесь!

  • Android SDK
    Введение в компоненты архитектуры Android
    Жестяная мегали

Пример проекта для этого урока можно найти в нашем репозитории GitHub, чтобы вы могли легко следить за ним.

Библиотека подкачки — это еще одна библиотека, добавленная к компонентам архитектуры . Библиотека помогает эффективно управлять загрузкой и отображением большого набора данных в RecyclerView . Согласно официальным документам:

Библиотека подкачки упрощает постепенную и плавную загрузку данных в RecyclerView вашего приложения.

Если какая-либо часть вашего приложения Android будет отображать большой набор данных из локального или удаленного источника данных, но отображать только его часть за раз, то вам следует рассмотреть возможность использования библиотеки подкачки. Это поможет улучшить производительность вашего приложения!

Теперь, когда вы увидели введение в библиотеку подкачки, вы можете спросить, зачем ее использовать? Вот несколько причин, по которым вам следует рассмотреть возможность его использования при загрузке больших наборов данных в RecyclerView .

  • Он не запрашивает данные, которые не нужны. Эта библиотека запрашивает только те данные, которые видны пользователю — когда пользователь прокручивает список.
  • Экономит батарею пользователя и потребляет меньше пропускной способности. Поскольку он запрашивает только те данные, которые необходимы, это экономит некоторые ресурсы устройства.

Это не будет эффективно при работе с большим объемом данных, так как базовый источник данных извлекает все данные, даже если пользователю будет отображаться только подмножество этих данных. В такой ситуации мы должны рассмотреть подкачку данных.

MainActivity Android Studio 3 и создайте новый проект с пустым действием MainActivity . Обязательно установите флажок Включить поддержку Kotlin .

Android Studio создать экран проекта

После создания нового проекта добавьте следующие зависимости в ваш 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, как найти библиотеку. Убедитесь, что вы не забыли синхронизировать свой проект после добавления их.

Создайте новый класс данных 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
}

Как вы знаете, для доступа к данным нашего приложения из библиотеки 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.

Вот как выглядит наш класс базы данных 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 имен (чтобы узнать больше, ознакомьтесь с примером исходного кода).

Чтобы наш пользовательский интерфейс мог хранить, наблюдать и обслуживать данные с учетом жизненного цикла, нам нужна 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> .

Чтобы показать наши данные 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
        }
 
    }
}

В нашем 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.

Если вы используете или хотите использовать RxJava в своем проекте, библиотека подкачки содержит еще один полезный артефакт: RxPagedListBuilder . Вы используете этот артефакт вместо LivePagedListBuilder для поддержки RxJava.

Вы просто создаете экземпляр RxPagedListBuilder , предоставляя те же аргументы, что и для LivePagedListBuilderDataSource.Factory и размер страницы. Затем вы вызываете buildObservable() или buildFlowable() чтобы вернуть Observable или Flowable для вашего PagedList соответственно.

Чтобы явно предоставить Scheduler для работы по загрузке данных, вы вызываете метод setFetchScheduler() . Чтобы также предоставить Scheduler для доставки результата (например, AndroidSchedulers.mainThread() ), просто вызовите setNotifyScheduler() . По умолчанию setNotifyScheduler() умолчанию используется для потока пользовательского интерфейса, а setFetchScheduler() для пула потоков ввода-вывода.

Из этого руководства вы узнали, как легко использовать компонент Paging из компонентов архитектуры Android (которые входят в состав Android Jetpack ) с Room. Это помогает нам эффективно загружать большие наборы данных из локальной базы данных, чтобы обеспечить более плавное взаимодействие с пользователем при прокрутке списка в RecyclerView .

Я настоятельно рекомендую ознакомиться с официальной документацией, чтобы узнать больше о библиотеке подкачки в Android.