استفاده از کتابخانه Paging با پایگاه داده Room در اپلیکیشن های اندرویدی – راهنمای جامع

۶۲ بازدید
آخرین به‌روزرسانی: ۱۹ شهریور ۱۴۰۲
زمان مطالعه: ۹ دقیقه

در این راهنما به شما نشان می‌دهیم که چگونه از کتابخانه Paging به عنوان یکی از کامپوننت‌های معماری اندروید به همراه پایگاه داده با پشتیبانی Room در یک اپلیکیشن اندروید استفاده کنید. شما در این مقاله با روش استفاده از کتابخانه Paging برای بارگذاری کارآمد مجموعه داده‌های بزرگ از پایگاه داده پشتیبانی شده توسط Room آشنا می‌شوید. بدین ترتیب کاربران اپلیکیشن شما هنگام اسکرول کردن یک RecyclerView تجربه کاربری روان‌تری را شاهد خواهند بود.

پیش‌نیازها

برای این که بتوانید از این راهنما استفاده کنید، باید ابتدا موارد زیر را فراهم کرده باشید:

  • نسخه 3.1.3 یا بالاتر
  • افزونه کاتلین Kotlin 1.2.51 یا بالاتر
  • همچنین باید درکی مقدماتی از کامپوننت‌های معماری اندروید (به خصوص LiveData و پایگاه داده Room داشته باشید)

کد اپلیکیشن ساده‌ای که در این نوشته ایجاد می‌شود در این ریپو گیت‌هاب قرار گرفته است و می‌توانید کد را از آنجا نیز پیگیری کنید.

کتابخانه Paging به چه معنا است؟

کتابخانه Paging یکی از کتابخانه‌های افزوده شده به کامپوننت‌های معماری اندروید محسوب می‌شود. این کتابخانه به مدیریت مؤثر بارگذاری و نمایش مجموعه داده‌های بزرگ در RecyclerView کمک می‌کند. تعریف رسمی آن چنین است:

کتابخانه Paging امکان بارگذاری تدریجی و روان داده‌ها در RecyclerView اپلیکیشن را مهیا می‌کند. اگر هر بخش از اپلیکیشن اندرویدی بخواهد مجموعه داده بزرگی را از منبع داده محلی یا ریموت نمایش دهد، هر زمان تنها بخشی از آن را می‌توان نمایش دهد. در این صورت باید از کتابخانه Paging کمک بگیرید تا عملکرد اپلیکیشن خود را بهبود ببخشید.

چرا باید از کتابخانه Paging استفاده کرد؟

پس از توضیحات مقدماتی فوق ممکن است از خود بپرسید، چرا باید از این کتابخانه استفاده کرد؟ در ادامه برخی دلایلی که چرا باید از آن برای بارگذاری مجموعه داده‌های بزرگ در یک RecyclerView استفاده کنید را توضیح داده‌ایم.

  • این کتابخانه، داده‌هایی را که لازم ندارد، درخواست نمی‌کند، یعنی این کتابخانه تنها داده‌هایی را می‌خواهد که هنگام اسکرول کردن کاربر، در دید وی قرار می‌گیرند.
  • باعث صرفه‌جویی در مصرف باتری و پهنای باند کاربر می‌شود. از آنجا این کتابخانه تنها داده‌های مورد تقاضا را نمایش می‌دهد باعث صرفه‌جویی در منابع گوشی تلفن همراه می‌شود.
  • بازیابی همه داده‌های مورد نیاز در زمان کار با حجم بالایی از داده‌ها رویه چندان کارآمدی به حساب نمی‌آید چون تنها زیرمجموعه کوچکی از آن هر زمان به کاربر نمایش داده می‌شود. در چنین موقعیتی باید به جای آن از صفحه‌بندی (paging) استفاده کنیم.

1. ایجاد یک پروژه اندروید استودیو

اندروید استودیو نسخه 3 خود را باز کنید و یک پروژه جدید با اکتیویتی خالی به نام MainActivity ایجاد کنید. مطمئن شوید که گزینه Include Kotlin support را انتخاب کرده‌اید.

2. افزودن کامپوننت‌های معماری

پس از ایجاد یک پروژه جدید، وابستگی‌های زیر را به بخش build.gradle پروژه خود اضافه کنید.

در این راهنما ما از آخرین نسخه (تا زمان نگارش این مقاله) از کتابخانه Paging و نسخه 1.1.1 Room استفاده می‌کنیم.

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

این اجزا در ریپازیتوری Maven گوگل موجود هستند.

allprojects {
    repositories {
        google()
        jcenter()
    }
}

با افزودن این وابستگی‌ها به Gradle می‌گوییم که چگونه کتابخانه را پیدا کند. مطمئن شوید که پس از افزودن موارد فوق، پروژه خود را sync می‌کنید.

3. ایجاد یک موجودیت (Entity)

یک کلاس داده جدید کاتلین به نام Person ایجاد کنید. به منظور ساده‌سازی امور، موجودیت Person ما دو فیلد دارد:

  • یک ID یکتا (id)
  • نام شخص (name)

به علاوه باید یک متد ()toString نیز استفاده کنیم تا این موجودیت مقدار name را باز گرداند.

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. ایجاد ADO

همان طور که می‌دانید برای این که بتوانیم از کتابخانه Room استفاده کنیم، ابتدا باید «اشیای دسترسی داده» (DAO) را در دست داشته باشیم. در این مورد ما یک DAO به نام PersonDao ایجاد می‌کنیم.

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>
 
    @Query("SELECT * FROM persons")
    fun getAllPaged(): DataSource.Factory<Int, Person>
 
    @Insert
    fun insertAll(persons: List)
 
    @Delete
    fun delete(person: Person)
}

در کلاس PersonDao دو متد Query@ داریم. یکی از آن‌ها به نام ()getAll است که مقدار LiveDateیی که فهرستی از اشیای Person را دارد باز می‌گرداند. دیگری متد ()getAllPaged است که یک DataSource.Factory باز می‌گرداند. بر اساس مستندات رسمی، کلاس DataSource یک کلاس پایه برای بارگذاری صفحه‌هایی از داده‌های گزینش شده درون یک PagedList است.

منظور از PagedList نوع خاصی از List است که داده‌های صفحه‌بندی شده را در اندروید نمایش می‌دهد. در واقع یک PagedList لیستی است که داده‌هایش را در دسته‌هایی (صفحه) از یک DataSource بارگذاری می‌کند. این موارد با متد (get(int قابل دسترس هستند و بارگذاری موارد دیگر با استفاده از متد (loadAroud(int میسر است.

ما متد استاتیک Factory در کلاس DataSource را فراخوانی می‌کنیم که به عنوان یک کارخانه (برای ایجاد اشیا، بدون در دست داشتن کلاس دقیق) برای DataSource عمل می‌کند. این متد استاتیک دو نوع داده می‌پذیرد:

  • کلیدی که آیتم‌های موجود در DataSource را می‌پذیرد. توجه داشته باشید که برای یک کوئری Room، صفحه‌ها شماره‌گذاری شده‌اند و از این رو باید از Integer به عنوان نوع شناسه صفحه استفاده کنید. امکان داشتن صفحه‌های «کلیددار» با استفاده از کتابخانه Paging وجود دارد؛ اما Room آن را در حال حاضر ارائه نمی‌کند.
  • نوعی از آیتم‌ها یا موجودیت‌ها (POJO ها) که در فهرست به وسیله DataSource ها بارگذاری می‌شوند.

5. ایجاد پایگاه داده

کلاس پایگاه داده Room چیزی شبیه زیر است:

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? = null
 
        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().build()
                            WorkManager.getInstance()?.enqueue(request)
                        }
                    })
                    .build()
        }
    }
}

در این جا ما یک وهله منفرد از پایگاه داده خود ایجاد کرده‌ایم و آن را با داده‌هایی که از API WorkManager استفاده می‌کنند، از قبل مقداردهی کرده‌ایم. توجه داشته باشید که داده‌هایی که از قبل مقداردهی شده‌اند تنها لیستی از 1000 نام را شامل می‌شوند. (برای مطالعه بیشتر به کد نمونه‌ای که ارائه شده است مراجعه کنید)

6. ایجاد ViewModel

برای این که UI ما بتواند داده‌ها را به روشی آگاه از چرخه عمر (lifecycle-conscious) ذخیره، مشاهده و عرضه بکند باید یک ViewModel داشته باشیم. PersonsViewModel ما که کلاس AndroidViewModel را بسط می‌دهد قرار است به عنوان ViewModel ما عمل کند.

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>
 
    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 از اشیای Person نگهداری می‌کند. به دلیل وجود همین LiveData است که UI (یعنی اکتیویتی یا فرگمنت) ما باید این داده‌ها را با فراخوانی متد getter برای ()getPersonsLiveData مشاهده کند.

ما personsLiveData را درون بلوک init مقداردهی اولیه کرده‌ایم. در این بلوک DataSource.Factory را با فراخوانی سینگلتون AppDatabase برای شیء PersonDao دریافت می‌کنیم. وقتی این شیء را به دست آوردیم، متد getAllPaged() را فراخوانی می‌کنیم.

سپس یک LivePagedListBuilder ایجاد می‌کنیم. مستندات رسمی در مورد آن چنین توضیح داده‌اند:

LivePagedListBuilder با فرض وجود LiveData<PagedList> و یک DataSource.Factory برای ساخت PagedList.Config استفاده می‌شود.

ما به متد سازنده آن DataSource.Factory را به عنوان نخستین آرگومان و اندازه صفحه را به عنوان دومین آرگومان ارسال می‌کنیم (در این مثال اندازه صفحه 50 خواهد بود). به طور معمول باید اندازه‌ای انتخاب کنید که از بیشینه تعدادی که هر بار برای کاربر نمایش خواهید داد بیشتر باشد. در نهایت متد build() را فراخوانی می‌کنیم تا این سازه را بسازد و به صورت یک LiveData<PagedList> برای ما بازگرداند.

7. ایجاد PagedListAdapter

برای نمایش این PagedList در یک RecyclerView به یک PagedListAdapter نیاز داریم.

در ادامه تعریف دقیقی از این کلاس را از مستندات رسمی ارائه کرده‌ایم. RecyclerView.Adapter یک کلاس مبنا برای ارائه داده‌های صفحه‌بندی شده از PagedLists ها در یک RecyclerView است. بنابراین یک PersonAdapter ایجاد می‌کنیم که PagedListAdapter را بسط می‌دهد.

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 و همچنین کلاسی که ViewHolder را برای استفاده از سوی آداپتور بسط می‌دهد را در سازنده آن تعیین کنید. در مورد مثال ما Person و PersonViewHolder را به ترتیب به عنوان آرگومان‌های اول و دوم ارسال می‌کنیم.

توجه داشته باشید PagedListAdapter شما را ملزم می‌کند که یک DiffUtil.ItemCallback به سازنده PageListAdapter ارسال کنید. DiffUtil یک کلاس کاربردی RecyclerView است که می‌تواند اختلاف بین دو لیست را محاسبه کرده و یک لیست از عملیات‌های به‌روزرسانی در خروجی ارائه کند که لیست نخست را به لیست دوم تبدیل می‌کند. ItemCallback یک کلاس استاتیک مجرد درونی است که برای محاسبه اختلاف بین دو آیتم غیر تهی در یک لیست استفاده می‌شود.

ما به طور خاص PersonDiffCallback را برای سازنده PagedListAdapter خود ارائه می‌کنیم.

import android.support.v7.util.DiffUtil
import com.chikeandroid.pagingtutsplus.data.Person
 
class PersonDiffCallback : DiffUtil.ItemCallback() {
 
    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 برای بررسی این که دوشی، آیتم یکسانی ارائه می‌دهند یا نه فراخوانی می‌شود. برای نمونه اگر آیتم‌های شما id های یکتایی داشته باشند، این متد کمیت id آن‌ها را بررسی می‌کند. این متد، در صورتی که دو آیتم، شیء یکسانی را نمایش دهند مقدار true و در مواردی که متفاوت باشند مقدار flase باز می‌گرداند.
  • areContentsTheSame برای بررسی این که آیا دو آیتم داده‌های یکسانی دارند فراخوانی می‌شود. در صورتی که محتوای دو آیتم یکسان باشد، این متد مقدار true و در غیر این صورت مقدار false باز می‌گرداند.

کلاس درونی PersonViewHolder ما تنها یک RecyclerView.ViewHolder نوعی است. این کلاس مسئول اتصال داده‌های مورد نیاز مدل به ویجت‌های یک ردیف در لیست ما است.

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().
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(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 است قرار دهد. اما توجه داشته باشید که همه منابع داده به سرعت بارگذاری نمی‌شوند. سرعت بارگذاری به توان پردازشی دستگاه اندرویدی نیز بستگی دارد.

9. یکپارچه‌سازی با RxJava

اگر از RxJava در پروژه خود استفاده می‌کنید یا قصد دارد چنین کنید، کتابخانه Paging یک بخش مفید دیگر نیز به نام RxPagedListBuilder دارد. از این بخش به جای LivePagedListBuilder برای پشتیبانی RxJava می‌توانید استفاده کنید.

کافی است یک وهله از RxPagedListBuilder ایجاد کنید که همان آرگومان‌هایی که برای LivePagedListBuilder وجود دارد ارائه می‌دهد، یعنی DataSource.Factory و اندازه صفحه. سپس می‌توانید به ترتیب ()buildObservable یا ()buildFlowable را برای بازگشت به عنوان یک Observable یا Flowable برای PagedList خود فراخوانی کنید.

برای ارائه صریح Scheduler برای عملیات بارگذاری داده‌ها می‌توانید متد setter را برای ()setFetchScheduler فراخوانی کنید. همچنین جهت ارائه Scheduler برای تحویل نتیجه کافی است ()setNotifyScheduler را فراخوانی کنید. به طور پیش‌فرض ()setNotifyScheduler به طور پیش‌فرض روی برای ترد UI اجرا می‌شود در حالی که () setFetchScheduler به طور پیش‌فرض روی استخر ترد I/O قرار دارد.

سخن پایانی

در این راهنما شیوه استفاده از کامپوننت Paging از میان کامپوننت‌های معماری اندروید (که بخشی از Android Jetpack است) را به همراه Room آموختیم. بدین ترتیب می‌توانیم مجموعه داده‌های حجیم را به طور کارآمدی از پایگاه‌های داده محلی بارگذاری کنیم تا هنگام اسکرول کردن در یک لیست در RecyclerView، تجربه کاربری روانی را در اختیار کاربر قرار دهیم.

قویاً توصیه می‌شود که مستندات رسمی را برای آموختن موارد بیشتر در مورد کتابخانه Paging در اندروید بررسی کنید.

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۳ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
tutsplus

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *