استفاده از کتابخانه 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 در اندروید بررسی کنید.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- آموزش برنامه نویسی اندروید (Android) – مقدماتی
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی اندروید (Android) – پیشرفته
- آموزش نصب اندروید استودیو (Android Studio)
- مجموعه آموزشهای مهندسی نرم افزار
- آموزش برنامه نویسی اندروید (Android) – تکمیلی
==