Android Room with a View - Kotlin

الغرض من مكوّنات البنية هو تقديم إرشادات بشأن بنية التطبيق، مع توفير مكتبات للمهام الشائعة، مثل إدارة مراحل النشاط واستمرار البيانات. تساعدك "مكوّنات البنية" في تنظيم تطبيقك بطريقة قوية وقابلة للاختبار والصيانة مع استخدام قدر أقل من الرموز النموذجية. تُعدّ مكتبات "مكوّنات الهندسة المتوافقة مع Android" جزءًا من Android Jetpack.

هذا هو إصدار Kotlin من الدرس العملي. يمكنك الاطّلاع على الإصدار بلغة البرمجة Java هنا.

إذا واجهت أي مشاكل (مثل أخطاء في الرمز أو أخطاء نحوية أو صياغة غير واضحة أو غير ذلك) أثناء العمل على هذا الدرس العملي، يُرجى الإبلاغ عن المشكلة من خلال الرابط الإبلاغ عن خطأ في أسفل يمين الدرس العملي.

المتطلبات الأساسية

يجب أن تكون على دراية بلغة Kotlin ومفاهيم التصميم المستند إلى الكائنات وأساسيات تطوير تطبيقات Android، وخاصةً ما يلي:

من المفيد أيضًا التعرّف على أنماط تصميم البرامج التي تفصل البيانات عن واجهة المستخدم، مثل MVP أو MVC. يطبِّق هذا الدرس التطبيقي حول الترميز البنية المحدّدة في دليل بنية التطبيق.

يركّز هذا الدرس التطبيقي حول الترميز على "مكوّنات الهندسة المتوافقة مع Android". يتم توفير المفاهيم والرموز البرمجية غير ذات الصلة لتتمكّن من نسخها ولصقها ببساطة.

إذا لم تكن على دراية بلغة Kotlin، يتوفّر إصدار من هذا الدرس التعليمي البرمجي بلغة Java هنا.

المهام التي ستنفذها

في هذا الدرس التطبيقي حول الترميز، ستتعلّم كيفية تصميم تطبيق وإنشائه باستخدام "مكوّنات البنية" Room وViewModel وLiveData، وإنشاء تطبيق ينفّذ ما يلي:

  • تنفِّذ البنية المقترَحة باستخدام "مكوّنات الهندسة المتوافقة مع Android".
  • تعمل مع قاعدة بيانات للحصول على البيانات وحفظها، وتعبئة قاعدة البيانات مسبقًا ببعض الكلمات.
  • تعرِض هذه السمة كل الكلمات في RecyclerView في MainActivity.
  • يفتح نشاطًا ثانيًا عندما ينقر المستخدم على الزر +. عندما يُدخل المستخدم كلمة، تتم إضافتها إلى قاعدة البيانات والقائمة.

التطبيق بسيط، ولكنه معقّد بما يكفي لاستخدامه كنموذج يمكنك البناء عليه. إليك معاينة:

المتطلبات

يوفّر لك هذا الدرس التطبيقي حول الترميز كل الرموز البرمجية التي تحتاج إليها لإنشاء التطبيق الكامل.

هناك العديد من الخطوات لاستخدام "مكوّنات البنية" وتنفيذ البنية المقترَحة. الأمر الأكثر أهمية هو إنشاء نموذج ذهني لما يحدث، وفهم كيفية تناسب الأجزاء معًا وكيفية تدفق البيانات. أثناء العمل على هذا الدرس التطبيقي، لا تكتفِ بنسخ الرمز البرمجي ولصقه، بل حاوِل فهمه جيدًا.

للتعريف بالمصطلحات، إليك مقدّمة قصيرة عن "مكوّنات البنية" وكيفية عملها معًا. يُرجى العِلم أنّ هذا الدرس العملي يركّز على مجموعة فرعية من المكوّنات، وهي LiveData وViewModel وRoom. يتم شرح كل مكوّن بشكل أكبر أثناء استخدامه.

يعرض هذا الرسم التخطيطي نموذجًا أساسيًا للبنية:

الكيان: فئة مشروحة تصف جدول قاعدة بيانات عند استخدام Room

قاعدة بيانات SQLite: يتم تخزينها على الجهاز. تنشئ مكتبة Room للبيانات الثابتة قاعدة البيانات هذه وتحتفظ بها نيابةً عنك.

كائن الوصول إلى البيانات (DAO): هو كائن يوفّر واجهة بين التطبيق وقاعدة البيانات. تتضمّن هذه السمة عملية ربط بين استعلامات SQL والدوال. عند استخدام كائن الوصول إلى البيانات، يمكنك استدعاء الطرق، ويتولّى Room بقية المهام.

قاعدة بيانات Room: تبسّط عملية استخدام قاعدة البيانات وتعمل كنقطة وصول إلى قاعدة بيانات SQLite الأساسية (تخفي SQLiteOpenHelper)). تستخدم قاعدة بيانات Room كائن الوصول إلى البيانات (DAO) لإصدار طلبات بحث إلى قاعدة بيانات SQLite.

المستودع: هو فئة تنشئها وتُستخدم بشكل أساسي لإدارة مصادر بيانات متعددة.

ViewModel: يعمل كمركز اتصال بين المستودع (البيانات) وواجهة المستخدم. لم يعُد على واجهة المستخدم القلق بشأن مصدر البيانات. تستمر مثيلات ViewModel بعد إعادة إنشاء النشاط/الجزء.

LiveData: فئة حاوية بيانات يمكن مراقبتها. يحتفظ دائمًا بأحدث إصدار من البيانات ويخزّنه مؤقتًا، ويُرسل إشعارات إلى المراقبين عند تغيُّر البيانات. ‫LiveData يراعي مراحل النشاط. تراقب مكوّنات واجهة المستخدم البيانات ذات الصلة فقط ولا تتوقف عن المراقبة أو تستأنفها. تتولّى LiveData إدارة كل ذلك تلقائيًا لأنّها على دراية بتغييرات حالة مراحل النشاط ذات الصلة أثناء المراقبة.

نظرة عامة على بنية RoomWordSample

يوضّح المخطّط التالي جميع أجزاء التطبيق. يمثّل كل مربع من المربعات المحيطة (باستثناء قاعدة بيانات SQLite) فئة ستنشئها.

  1. افتح "استوديو Android" وانقر على بدء مشروع جديد في "استوديو Android".
  2. في نافذة "إنشاء مشروع جديد"، اختَر نشاط فارغ وانقر على التالي.
  3. في الشاشة التالية، أطلِق على التطبيق اسم RoomWordSample، ثم انقر على إنهاء.

بعد ذلك، عليك إضافة مكتبات المكوّنات إلى ملفات Gradle.

  1. في "استوديو Android"، انقر على علامة التبويب "المشاريع" (Projects) ووسِّع مجلد "برامج Gradle النصية" (Gradle Scripts).

افتح build.gradle (الوحدة: app).

  1. طبِّق المكوّن الإضافي لمعالج التعليقات التوضيحية kapt في Kotlin عن طريق إضافته بعد المكوّنات الإضافية الأخرى المحدّدة في أعلى ملف build.gradle (الوحدة: التطبيق).
apply plugin: 'kotlin-kapt'
  1. أضِف كتلة packagingOptions داخل كتلة android لاستبعاد وحدة الدوال الذرية من الحزمة ومنع ظهور التحذيرات.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. أضِف الرمز التالي في نهاية الحظر dependencies.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"

// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
  1. في ملف build.gradle (Project: RoomWordsSample)، أضِف أرقام الإصدارات إلى نهاية الملف، كما هو موضّح في الرمز البرمجي أدناه.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

بيانات هذا التطبيق هي كلمات، وستحتاج إلى جدول بسيط لتخزين هذه القيم:

تتيح لك Room إنشاء جداول من خلال كيان. لنبدأ الآن.

  1. أنشئ ملف فئة Kotlin جديدًا باسم Word يحتوي على Word فئة البيانات.
    سيصف هذا الصف الكيان (الذي يمثّل جدول SQLite) الخاص بكلماتك. يمثّل كل سمة في الفئة عمودًا في الجدول. سيستخدم Room في النهاية هذه الخصائص لإنشاء الجدول وإنشاء مثيلات للكائنات من الصفوف في قاعدة البيانات.

إليك الرمز:

data class Word(val word: String)

لجعل فئة Word مفيدة لقاعدة بيانات Room، عليك إضافة تعليق توضيحي إليها. تحدّد التعليقات التوضيحية كيفية ارتباط كل جزء من هذه الفئة بإدخال في قاعدة البيانات. يستخدم Room هذه المعلومات لإنشاء الرمز.

إذا كتبت التعليقات التوضيحية بنفسك (بدلاً من لصقها)، سيستورد Android Studio تلقائيًا فئات التعليقات التوضيحية.

  1. عدِّل فئة Word بإضافة التعليقات التوضيحية كما هو موضّح في الرمز التالي:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

في ما يلي شرح لوظيفة هذه التعليقات التوضيحية:

  • @Entity(tableName = "word_table")
    يمثّل كل فئة @Entity جدول SQLite. أضِف تعليقًا توضيحيًا إلى تعريف الفئة للإشارة إلى أنّها كيان. يمكنك تحديد اسم الجدول إذا أردت أن يكون مختلفًا عن اسم الفئة. يؤدي ذلك إلى تسمية الجدول "word_table".
  • @PrimaryKey
    يحتاج كل عنصر إلى مفتاح أساسي. لتبسيط الأمور، تعمل كل كلمة كمفتاح أساسي خاص بها.
  • @ColumnInfo(name = "word")
    تحدّد هذه السمة اسم العمود في الجدول إذا أردت أن يكون مختلفًا عن اسم المتغيّر التابع. يؤدي ذلك إلى تسمية العمود "كلمة".
  • يجب أن يكون كل موقع يتم تخزينه في قاعدة البيانات مرئيًا للجميع، وهو الإعداد التلقائي في Kotlin.

يمكنك العثور على قائمة كاملة بالتعليقات التوضيحية في مرجع ملخّص حزمة Room.

ما هي المنظمة المستقلة اللامركزية؟

في DAO (كائن الوصول إلى البيانات)، يمكنك تحديد استعلامات SQL وربطها باستدعاءات الطرق. يتحقّق المحوّل البرمجي من SQL وينشئ طلبات بحث من التعليقات التوضيحية المريحة لطلبات البحث الشائعة، مثل @Insert. تستخدم Room كائن الوصول إلى البيانات لإنشاء واجهة برمجة تطبيقات نظيفة للرمز البرمجي.

يجب أن تكون DAO واجهة أو فئة مجرّدة.

يجب تنفيذ جميع طلبات البحث تلقائيًا في سلسلة محادثات منفصلة.

تتيح مكتبة Room استخدام الروتينات المشتركة، ما يسمح بإضافة التعليقات التوضيحية إلى طلبات البحث باستخدام المعدِّل suspend ثم استدعاؤها من روتين مشترك أو من دالة تعليق أخرى.

تنفيذ DAO

لنكتب DAO يوفّر طلبات بحث عن:

  • الحصول على جميع الكلمات مرتّبة أبجديًا
  • إدراج كلمة
  • حذف كل الكلمات
  1. أنشئ ملف فئة Kotlin جديدًا باسم WordDao.
  2. انسخ الرمز التالي والصِقه في WordDao وعدِّل عمليات الاستيراد حسب الحاجة لكي يتم تجميعه.
@Dao
interface WordDao {

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAlphabetizedWords(): List<Word>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)

    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}

لنستعرض الخطوات:

  • WordDao هي واجهة، ويجب أن تكون كائنات الوصول إلى البيانات إما واجهات أو فئات مجرّدة.
  • تحدّد التعليق التوضيحي @Dao أنّها فئة DAO في Room.
  • suspend fun insert(word: Word) : تعرِّف دالة تعليق لإدراج كلمة واحدة.
  • @Insert التعليق التوضيحي هو تعليق توضيحي خاص بطريقة DAO لا تحتاج فيه إلى تقديم أي SQL. (هناك أيضًا التعليقان التوضيحيان @Delete و@Update لحذف الصفوف وتعديلها، ولكنك لا تستخدمهما في هذا التطبيق).
  • onConflict = OnConflictStrategy.IGNORE: تتجاهل استراتيجية onConflict المحدّدة كلمة جديدة إذا كانت مطابقة تمامًا لكلمة موجودة في القائمة. لمزيد من المعلومات عن استراتيجيات التعارض المتاحة، اطّلِع على المستندات.
  • suspend fun deleteAll(): تعرّف هذه السمة دالة تعليق لحذف جميع الكلمات.
  • لا تتوفّر تعليقات توضيحية سهلة لحذف عناصر متعدّدة، لذا يتمّ استخدام التعليق التوضيحي العام @Query.
  • @Query("DELETE FROM word_table"): يتطلّب @Query منك تقديم طلب بحث SQL كمعلَمة سلسلة إلى التعليق التوضيحي، ما يتيح طلبات بحث معقّدة للقراءة وعمليات أخرى.
  • fun getAlphabetizedWords(): List<Word>: طريقة للحصول على جميع الكلمات وإرجاع List من Words.
  • @Query("SELECT * from word_table ORDER BY word ASC"): طلب بحث يعرض قائمة بالكلمات مرتّبة ترتيبًا تصاعديًا.

عندما تتغيّر البيانات، عليك عادةً اتّخاذ بعض الإجراءات، مثل عرض البيانات المعدَّلة في واجهة المستخدم. وهذا يعني أنّه عليك مراقبة البيانات حتى تتمكّن من التفاعل عند تغيُّرها.

قد يكون ذلك صعبًا حسب طريقة تخزين البيانات. يمكن أن يؤدي رصد التغييرات في البيانات على مستوى عدّة مكوّنات في تطبيقك إلى إنشاء مسارات تبعية صريحة وثابتة بين المكوّنات. ويؤدي ذلك إلى صعوبة الاختبار وتصحيح الأخطاء، وغير ذلك.

LiveData، وهو فئة مكتبة دورة الحياة لمراقبة البيانات، يحلّ هذه المشكلة. استخدِم قيمة إرجاع من النوع LiveData في وصف طريقتك، وسينشئ Room كل الرموز اللازمة لتعديل LiveData عند تعديل قاعدة البيانات.

في WordDao، غيِّر توقيع طريقة getAlphabetizedWords() بحيث يتم تضمين List<Word> الذي تم إرجاعه في LiveData.

   @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): LiveData<List<Word>>

في وقت لاحق من هذا الدرس العملي، ستتتبّع تغييرات البيانات من خلال Observer في MainActivity.

ما هي قاعدة بيانات Room؟

  • ‫Room هي طبقة قاعدة بيانات تستند إلى قاعدة بيانات SQLite.
  • تتولّى Room المهام الروتينية التي كنت تنفّذها باستخدام SQLiteOpenHelper.
  • يستخدم Room كائن الوصول إلى البيانات (DAO) لإصدار طلبات بحث إلى قاعدة البيانات.
  • بشكلٍ تلقائي، لتجنُّب ضعف أداء واجهة المستخدم، لا يسمح لك Room بإصدار طلبات بحث في سلسلة التعليمات الرئيسية. عندما تعرض طلبات بحث Room LiveData، يتم تنفيذ طلبات البحث تلقائيًا بشكل غير متزامن في سلسلة محادثات في الخلفية.
  • توفّر Room عمليات تحقّق في وقت التجميع لعبارات SQLite.

تنفيذ قاعدة بيانات Room

يجب أن يكون صف قاعدة بيانات Room مجرّدًا وأن يمتد إلى RoomDatabase. عادةً، تحتاج إلى نسخة واحدة فقط من قاعدة بيانات Room للتطبيق بأكمله.

لننشئ واحدًا الآن.

  1. أنشئ ملف فئة Kotlin باسم WordRoomDatabase وأضِف الرمز التالي إليه:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time. 
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java, 
                        "word_database"
                    ).build()
                INSTANCE = instance
                return instance
            }
        }
   }
}

لنتعرّف على التعليمات البرمجية:

  • يجب أن يكون فئة قاعدة البيانات في Room هي abstract وأن يتم توسيعها لتشمل RoomDatabase
  • يمكنك إضافة التعليق التوضيحي @Database إلى الفئة لتصبح قاعدة بيانات Room، واستخدام مَعلمات التعليق التوضيحي للإعلان عن الكيانات التي تنتمي إلى قاعدة البيانات وتحديد رقم الإصدار. يتوافق كل عنصر مع جدول سيتم إنشاؤه في قاعدة البيانات. لا تتناول هذه السلسلة التعليمية عمليات نقل البيانات في قاعدة البيانات، لذا سنضبط قيمة exportSchema على false هنا لتجنُّب ظهور تحذير أثناء الإنشاء. في تطبيق حقيقي، عليك التفكير في ضبط دليل لاستخدامه في Room لتصدير المخطط حتى تتمكّن من التحقّق من المخطط الحالي في نظام التحكّم في الإصدار.
  • تعرض قاعدة البيانات عناصر DAO من خلال طريقة "getter" مجرّدة لكل @Dao.
  • لقد حدّدنا نمط تصميم singleton، WordRoomDatabase, لمنع فتح عدة مثيلات من قاعدة البيانات في الوقت نفسه.
  • تعرض الدالة getDatabase العنصر الفردي. سيتم إنشاء قاعدة البيانات عند الوصول إليها لأول مرة، وذلك باستخدام أداة إنشاء قاعدة البيانات في Room لإنشاء عنصر RoomDatabase في سياق التطبيق من الفئة WordRoomDatabase وتسميته "word_database".

ما هو المستودع؟

تجرِّد فئة المستودع إمكانية الوصول إلى مصادر بيانات متعددة. لا يشكّل المستودع جزءًا من مكتبات "مكوّنات البنية"، ولكنّه يُعدّ من أفضل الممارسات المقترَحة لفصل الرموز البرمجية والبنية. يوفّر فئة Repository واجهة برمجة تطبيقات واضحة للوصول إلى البيانات لبقية التطبيق.

لماذا يجب استخدام مستودع؟

يدير المستودع طلبات البحث ويسمح لك باستخدام عدة أنظمة خلفية. في المثال الأكثر شيوعًا، ينفّذ مستودع البيانات منطق تحديد ما إذا كان سيتم جلب البيانات من شبكة أو استخدام النتائج المخزّنة مؤقتًا في قاعدة بيانات محلية.

تنفيذ المستودع

أنشئ ملف فئة Kotlin باسم WordRepository والصِق الرمز التالي فيه:

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed LiveData will notify the observer when the data has changed.
    val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
 
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

النقاط الرئيسية:

  • يتم تمرير DAO إلى أداة إنشاء المستودع بدلاً من قاعدة البيانات بأكملها. ويرجع ذلك إلى أنّها تحتاج فقط إلى الوصول إلى DAO، لأنّ DAO يحتوي على جميع طرق القراءة والكتابة لقاعدة البيانات. ليست هناك حاجة إلى عرض قاعدة البيانات بأكملها على المستودع.
  • قائمة الكلمات هي سمة عامة. يتم تهيئته من خلال الحصول على قائمة LiveData بالكلمات من Room، ويمكننا إجراء ذلك بسبب طريقة تحديدنا للطريقة getAlphabetizedWords لعرض LiveData في خطوة "فئة LiveData". ينفّذ Room جميع طلبات البحث في سلسلة محادثات منفصلة. بعد ذلك، سيُرسِل LiveData الذي تم رصده إشعارًا إلى المراقب في سلسلة التعليمات الرئيسية عند تغيير البيانات.
  • يُعلم المعدِّل suspend المترجم بأنّه يجب استدعاء هذا المعدِّل من روتين فرعي أو دالة تعليق أخرى.

ما هي ViewModel؟

دور ViewModel هو توفير البيانات لواجهة المستخدم والحفاظ عليها عند إجراء تغييرات في الإعداد. يعمل ViewModel كمركز اتصال بين المستودع وواجهة المستخدم. يمكنك أيضًا استخدام ViewModel لمشاركة البيانات بين الأجزاء. ‫ViewModel هو جزء من مكتبة دورة الحياة.

للحصول على دليل تمهيدي حول هذا الموضوع، اطّلِع على ViewModel Overview أو مشاركة المدوّنة ViewModels: A Simple Example.

لماذا يجب استخدام ViewModel؟

تحتفظ ViewModel ببيانات واجهة المستخدم لتطبيقك بطريقة تراعي مراحل النشاط وتتجاوز تغييرات الإعدادات. يسمح لك فصل بيانات واجهة المستخدم لتطبيقك عن الفئتَين Activity وFragment باتّباع مبدأ المسؤولية الفردية بشكل أفضل: تكون الأنشطة واللقطات مسؤولة عن عرض البيانات على الشاشة، بينما يمكن أن تتولّى ViewModel مهمة الاحتفاظ بجميع البيانات اللازمة لواجهة المستخدم ومعالجتها.

في ViewModel، استخدِم LiveData للبيانات القابلة للتغيير التي ستستخدمها واجهة المستخدم أو تعرضها. يوفّر استخدام LiveData العديد من المزايا:

  • يمكنك وضع مراقب على البيانات (بدلاً من طلب التغييرات بشكل متكرّر) وتعديل
    واجهة المستخدم فقط عند حدوث تغيير في البيانات.
  • يتم فصل المستودع وواجهة المستخدم تمامًا باستخدام ViewModel.
  • لا يتم إجراء أي طلبات من قاعدة البيانات من ViewModel (يتم التعامل مع كل ذلك في المستودع)، ما يجعل الرمز البرمجي قابلاً للاختبار بشكل أكبر.

viewModelScope

في Kotlin، يتم تشغيل جميع الروتينات الفرعية داخل CoroutineScope. يتحكّم النطاق في مدة بقاء الروتينات الفرعية من خلال مهمتها. عند إلغاء مهمة نطاق، يتم إلغاء جميع الروتينات الفرعية التي تم بدءها في هذا النطاق.

تضيف مكتبة AndroidX lifecycle-viewmodel-ktx viewModelScope كدالة إضافية لفئة ViewModel، ما يتيح لك استخدام النطاقات.

لمزيد من المعلومات حول استخدام الكوروتينات في ViewModel، يمكنك الاطّلاع على الخطوة 5 من الدرس التطبيقي حول الترميز استخدام الكوروتينات في Kotlin في تطبيق Android أو مشاركة منشور المدونة "الكوروتينات السهلة في Android: viewModelScope".

تنفيذ ViewModel

أنشئ ملف فئة Kotlin لـ WordViewModel وأضِف الرمز التالي إليه:

class WordViewModel(application: Application) : AndroidViewModel(application) {

    private val repository: WordRepository
    // Using LiveData and caching what getAlphabetizedWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allWords: LiveData<List<Word>>

    init {
        val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */
    fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
        repository.insert(word)
    }
}

في ما يلي:

  • تم إنشاء فئة باسم WordViewModel تتلقّى Application كمَعلمة وتوسّع AndroidViewModel.
  • تمت إضافة متغيّر عضو خاص للاحتفاظ بمرجع إلى المستودع.
  • تمت إضافة متغيّر عضو LiveData عام لتخزين قائمة الكلمات مؤقتًا.
  • تم إنشاء حظر init يحصل على إشارة إلى WordDao من WordRoomDatabase.
  • في المربّع init، تم إنشاء WordRepository استنادًا إلى WordRoomDatabase.
  • في الحزمة init، تمّت تهيئة allWords LiveData باستخدام المستودع.
  • تم إنشاء طريقة insert() لتغليف البيانات تستدعي طريقة insert() في المستودع. بهذه الطريقة، يتم تغليف تنفيذ insert() من واجهة المستخدم. لا نريد أن تحظر عملية الإدراج سلسلة التعليمات الرئيسية، لذلك سنطلق روتينًا فرعيًا جديدًا ونستدعي عملية الإدراج في المستودع، وهي دالة تعليق. كما ذكرنا، تحتوي ViewModels على نطاق روتين فرعي يستند إلى دورة حياتها ويُسمى viewModelScope، وهو ما نستخدمه هنا.

بعد ذلك، عليك إضافة تنسيق XML للقائمة والعناصر.

يفترض هذا الدرس العملي أنّك على دراية بإنشاء التصاميم في XML، لذا سنقدّم لك الرمز البرمجي فقط.

اجعل مظهر تطبيقك متوافقًا مع Material Design من خلال ضبط العنصر الرئيسي AppTheme على Theme.MaterialComponents.Light.DarkActionBar. أضِف نمطًا لعناصر القائمة في values/styles.xml:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- The default font for RecyclerView items is too small.
    The margin is a simple delimiter between the words. -->
    <style name="word_title">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_marginBottom">8dp</item>
        <item name="android:paddingLeft">8dp</item>
        <item name="android:background">@android:color/holo_orange_light</item>
        <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
    </style>
</resources>

إضافة تصميم layout/recyclerview_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        style="@style/word_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light" />
</LinearLayout>

في layout/activity_main.xml، استبدِل TextView بـ RecyclerView وأضِف زر إجراء عائمًا (FAB). يجب أن يبدو التنسيق الآن على النحو التالي:

<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=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/recyclerview_item"
        android:padding="@dimen/big_padding"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"/>

</androidx.constraintlayout.widget.ConstraintLayout>

يجب أن يتوافق مظهر زر الإجراء العائم مع الإجراء المتاح، لذا سنستبدل الرمز بعلامة "+".

أولاً، علينا إضافة Vector Asset جديد:

  1. اختَر ملف > جديد > عنصر متّجه.
  2. انقر على رمز روبوت Android في الحقل قصاصة فنية: .
  3. ابحث عن "إضافة" واختَر مادة العرض "+". انقر على حسنًا
    .
  4. بعد ذلك، انقر على التالي.
  5. أكِّد مسار الرمز على أنّه main > drawable وانقر على إنهاء لإضافة مادة العرض.
  6. في layout/activity_main.xml، عدِّل الزر العائم لإضافة العنصر الجديد القابل للرسم:
<com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"
        android:src="@drawable/ic_add_black_24dp"/>

سنعرض البيانات في RecyclerView، وهو أفضل من عرض البيانات في TextView. يفترض هذا الدرس العملي أنّك تعرف طريقة عمل RecyclerView وRecyclerView.LayoutManager وRecyclerView.ViewHolder وRecyclerView.Adapter.

يُرجى العِلم أنّ المتغيّر words في أداة الربط يخزّن البيانات مؤقتًا. في المهمة التالية، ستضيف الرمز الذي يعدّل البيانات تلقائيًا.

أنشئ ملف فئة Kotlin لـ WordListAdapter يوسّع RecyclerView.Adapter. إليك الرمز:

class WordListAdapter internal constructor(
        context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var words = emptyList<Word>() // Cached copy of words

    inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = words[position]
        holder.wordItemView.text = current.word
    }

    internal fun setWords(words: List<Word>) {
        this.words = words
        notifyDataSetChanged()
    }

    override fun getItemCount() = words.size
}

أضِف RecyclerView في طريقة onCreate() الخاصة بـ MainActivity.

في الطريقة onCreate() بعد setContentView:

   val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
   val adapter = WordListAdapter(this)
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

شغِّل تطبيقك للتأكّد من أنّ كل شيء يعمل. لا تتوفّر أي عناصر لأنّك لم تربط البيانات بعد.

لا تتضمّن قاعدة البيانات أي بيانات. ستضيف البيانات بطريقتَين: إضافة بعض البيانات عند فتح قاعدة البيانات، وإضافة Activity لإضافة الكلمات.

لحذف كل المحتوى وإعادة ملء قاعدة البيانات كلما تم تشغيل التطبيق، عليك إنشاء RoomDatabase.Callback وتجاوز onOpen(). بما أنّه لا يمكنك تنفيذ عمليات قاعدة بيانات Room على سلسلة التعليمات الخاصة بواجهة المستخدم، فإنّ onOpen() تُطلق روتينًا فرعيًا على IO Dispatcher.

لتشغيل روتين فرعي، نحتاج إلى CoroutineScope. عدِّل طريقة getDatabase في الفئة WordRoomDatabase للحصول أيضًا على نطاق روتين فرعي كمعلَمة:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

عدِّل أداة تهيئة استرجاع قاعدة البيانات في الحظر init من WordViewModel لتمرير النطاق أيضًا:

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

في WordRoomDatabase، ننشئ عملية تنفيذ مخصّصة لـ RoomDatabase.Callback()، والتي تحصل أيضًا على CoroutineScope كمعلّمة للدالة الإنشائية. بعد ذلك، نتجاوز طريقة onOpen لتعبئة قاعدة البيانات.

في ما يلي الرمز البرمجي لإنشاء دالة رد الاتصال داخل الفئة WordRoomDatabase:

private class WordDatabaseCallback(
    private val scope: CoroutineScope
) : RoomDatabase.Callback() {

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }

    suspend fun populateDatabase(wordDao: WordDao) {
        // Delete all content here.
        wordDao.deleteAll()

        // Add sample words.
        var word = Word("Hello")
        wordDao.insert(word)
        word = Word("World!")
        wordDao.insert(word)

        // TODO: Add your own words!
    }
}

أخيرًا، أضِف دالة معاودة الاتصال إلى تسلسل إنشاء قاعدة البيانات قبل استدعاء .build() مباشرةً في Room.databaseBuilder():

.addCallback(WordDatabaseCallback(scope))

في ما يلي الشكل الذي يجب أن يبدو عليه الرمز النهائي:

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onOpen(db: SupportSQLiteDatabase) {
           super.onOpen(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // Delete all content here.
                   wordDao.deleteAll()

                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)

                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }

   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

أضِف موارد السلسلة هذه في values/strings.xml:

<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>

أضِف مرجع اللون هذا في value/colors.xml:

<color name="buttonLabel">#FFFFFF</color>

أنشئ ملف موارد جديدًا للأبعاد:

  1. انقر على وحدة التطبيق في نافذة المشروع.
  2. اختَر ملف > جديد > ملف موارد Android
  3. من "المؤهّلات المتاحة"، اختَر السمة .
  4. ضبط اسم الملف: dimens

أضِف مراجع السمات هذه في values/dimens.xml:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

أنشئ مشروع Android Activity فارغًا باستخدام نموذج Empty Activity:

  1. اختَر ملف > جديد > نشاط > نشاط فارغ
  2. أدخِل NewWordActivity في حقل "اسم النشاط".
  3. تأكَّد من إضافة النشاط الجديد إلى ملف AndroidManifest.xml.
<activity android:name=".NewWordActivity"></activity>

عدِّل ملف activity_new_word.xml في مجلد التصميم باستخدام الرمز التالي:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/edit_word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="@dimen/min_height"
        android:fontFamily="sans-serif-light"
        android:hint="@string/hint_word"
        android:inputType="textAutoComplete"
        android:layout_margin="@dimen/big_padding"
        android:textSize="18sp" />

    <Button
        android:id="@+id/button_save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="@string/button_save"
        android:layout_margin="@dimen/big_padding"
        android:textColor="@color/buttonLabel" />

</LinearLayout>

عدِّل رمز النشاط:

class NewWordActivity : AppCompatActivity() {

    private lateinit var editWordView: EditText

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        editWordView = findViewById(R.id.edit_word)

        val button = findViewById<Button>(R.id.button_save)
        button.setOnClickListener {
            val replyIntent = Intent()
            if (TextUtils.isEmpty(editWordView.text)) {
                setResult(Activity.RESULT_CANCELED, replyIntent)
            } else {
                val word = editWordView.text.toString()
                replyIntent.putExtra(EXTRA_REPLY, word)
                setResult(Activity.RESULT_OK, replyIntent)
            }
            finish()
        }
    }

    companion object {
        const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
    }
}

الخطوة الأخيرة هي ربط واجهة المستخدم بقاعدة البيانات من خلال حفظ الكلمات الجديدة التي يدخلها المستخدم وعرض المحتوى الحالي لقاعدة بيانات الكلمات في RecyclerView.

لعرض المحتوى الحالي لقاعدة البيانات، أضِف مراقبًا يراقب LiveData في ViewModel.

عندما تتغيّر البيانات، يتم استدعاء معاودة الاتصال onChanged()، ما يؤدي إلى استدعاء طريقة setWords() الخاصة بالمحوّل لتعديل البيانات المخزّنة مؤقتًا في المحوّل وإعادة تحميل القائمة المعروضة.

في MainActivity، أنشئ متغيّر عضو لـ ViewModel:

private lateinit var wordViewModel: WordViewModel

استخدِم ViewModelProvider لربط ViewModel بموقعك الإلكتروني Activity.

عندما يبدأ Activity للمرة الأولى، سيُنشئ ViewModelProviders ViewModel. عند إيقاف النشاط، مثلاً من خلال تغيير الإعداد، يستمر ViewModel. عند إعادة إنشاء النشاط، ستعرض ViewModelProviders ViewModel الحالي. لمزيد من المعلومات، يُرجى الاطّلاع على ViewModel.

في onCreate() أسفل مجموعة الرموز RecyclerView، احصل على ViewModel من ViewModelProvider:

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

في onCreate() أيضًا، أضِف مراقبًا للسمة allWords LiveData من WordViewModel.
.

يتم تشغيل طريقة onChanged() (الطريقة التلقائية لـ Lambda) عند تغيُّر البيانات المرصودة ويكون النشاط في المقدّمة:

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

نريد فتح NewWordActivity عند النقر على زر الإجراء العائم، وعند العودة إلى MainActivity، نريد إما إدراج الكلمة الجديدة في قاعدة البيانات أو عرض Toast. لتحقيق ذلك، لنبدأ بتحديد رمز الطلب:

private val newWordActivityRequestCode = 1

في MainActivity، أضِف الرمز onActivityResult() الخاص بـ NewWordActivity.

إذا عاد النشاط مع RESULT_OK، أدرِج الكلمة التي تم إرجاعها في قاعدة البيانات عن طريق استدعاء طريقة insert() الخاصة بـ WordViewModel:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
        data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
            val word = Word(it)
            wordViewModel.insert(word)
        }
    } else {
        Toast.makeText(
            applicationContext,
            R.string.empty_not_saved,
            Toast.LENGTH_LONG).show()
    }
}

في MainActivity,البداية NewWordActivity عندما ينقر المستخدم على زر الإجراء العائم في MainActivity onCreate، ابحث عن زر الإجراء العائم وأضِف onClickListener باستخدام الرمز التالي:

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

يجب أن تبدو التعليمات البرمجية المكتملة على النحو التالي:

class MainActivity : AppCompatActivity() {

   private const val newWordActivityRequestCode = 1
   private lateinit var wordViewModel: WordViewModel

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

       val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
       val adapter = WordListAdapter(this)
       recyclerView.adapter = adapter
       recyclerView.layoutManager = LinearLayoutManager(this)

       wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
       wordViewModel.allWords.observe(this, Observer { words ->
           // Update the cached copy of the words in the adapter.
           words?.let { adapter.setWords(it) }
       })

       val fab = findViewById<FloatingActionButton>(R.id.fab)
       fab.setOnClickListener {
           val intent = Intent(this@MainActivity, NewWordActivity::class.java)
           startActivityForResult(intent, newWordActivityRequestCode)
       }
   }

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)

       if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
           data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
               val word = Word(it)
               wordViewModel.insert(word)
           }
       } else {
           Toast.makeText(
               applicationContext,
               R.string.empty_not_saved,
               Toast.LENGTH_LONG).show()
       }
   }
}

شغِّل تطبيقك الآن. عند إضافة كلمة إلى قاعدة البيانات في NewWordActivity، سيتم تعديل واجهة المستخدِم تلقائيًا.

بعد أن أصبح لديك تطبيق يعمل، لنلخّص ما أنشأته. في ما يلي بنية التطبيق مرة أخرى:

مكوّنات التطبيق هي:

  • MainActivity: تعرض الكلمات في قائمة باستخدام RecyclerView و WordListAdapter. في MainActivity، هناك Observer يراقب الكلمات LiveData من قاعدة البيانات ويتم إعلامه عند تغييرها.
  • تضيف NewWordActivity: كلمة جديدة إلى القائمة.
  • WordViewModel: يوفّر طرقًا للوصول إلى طبقة البيانات، ويعرض LiveData حتى تتمكّن MainActivity من إعداد علاقة المراقبة.*
  • LiveData<List<Word>>: يتيح إجراء التحديثات التلقائية في مكوّنات واجهة المستخدم. في MainActivity، هناك Observer يراقب كلمات LiveData من قاعدة البيانات ويتم إعلامه عند تغييرها.
  • يدير حساب Repository: مصدر بيانات واحدًا أو أكثر. تعرض Repository طرقًا لتتفاعل ViewModel مع مقدّم البيانات الأساسي. في هذا التطبيق، تكون الخلفية عبارة عن قاعدة بيانات Room.
  • Room: هو برنامج تضمين لقاعدة بيانات SQLite وينفّذها. يوفّر لك Room الكثير من العمل الذي كان عليك إنجازه بنفسك.
  • كائن الوصول إلى البيانات: يربط استدعاءات الطرق بطلبات البحث في قاعدة البيانات، وبالتالي عندما يستدعي المستودع طريقة مثل getAlphabetizedWords()، يمكن لـ Room تنفيذ SELECT * from word_table ORDER BY word ASC.
  • Word: هي فئة الكيان التي تحتوي على كلمة واحدة.

* تتفاعل Views وActivitiesFragments) فقط مع البيانات من خلال ViewModel. وبالتالي، لا يهم مصدر البيانات.

تدفّق البيانات لتحديثات واجهة المستخدم التلقائية (واجهة المستخدم التفاعلية)

يمكن إجراء التحديث التلقائي لأنّنا نستخدم LiveData. في MainActivity، هناك Observer يراقب كلمات LiveData من قاعدة البيانات ويتم إعلامه عند تغييرها. عند حدوث تغيير، يتم تنفيذ طريقة onChange() الخاصة بالمراقب ويتم تعديل mWords في WordListAdapter.

يمكن ملاحظة البيانات لأنّها LiveData. والقيمة التي يتم رصدها هي LiveData<List<Word>> التي تعرضها السمة WordViewModel allWords.

يخفي WordViewModel كل ما يتعلق الخلفية عن طبقة واجهة المستخدم. توفّر هذه السمة طرقًا للوصول إلى طبقة البيانات، وتعرض القيمة LiveData حتى تتمكّن السمة MainActivity من إعداد علاقة المراقبة. تتفاعل Views وActivitiesFragments) فقط مع البيانات من خلال ViewModel. وبالتالي، لا يهم مصدر البيانات.

في هذه الحالة، يكون مصدر البيانات Repository. لا يحتاج ViewModel إلى معرفة ما يتفاعل معه هذا المستودع. كل ما يحتاج إليه هو معرفة كيفية التفاعل مع Repository، وذلك من خلال الطرق التي يعرضها Repository.

يدير المستودع مصدر بيانات واحدًا أو أكثر. في تطبيق WordListSample، تكون قاعدة البيانات الخلفية هي قاعدة بيانات Room. ‫Room هي أداة تغليف لقاعدة بيانات SQLite وتنفّذها. يوفّر لك Room الكثير من العمل الذي كان عليك إنجازه بنفسك. على سبيل المثال، يتيح لك Room تنفيذ كل ما كنت تفعله باستخدام SQLiteOpenHelper صف.

تُجري DAO عمليات ربط بين طلبات استدعاء الدوال واستعلامات قاعدة البيانات، وبالتالي عندما يستدعي المستودع دالة مثل getAllWords()، يمكن لـ Room تنفيذ SELECT * from word_table ORDER BY word ASC.

بما أنّ النتيجة التي يتم عرضها من طلب البحث هي LiveData، في كل مرة تتغيّر فيها البيانات في Room، يتم تنفيذ طريقة onChanged() في واجهة Observer ويتم تعديل واجهة المستخدم.

[اختياري] تنزيل رمز الحلّ

إذا لم يسبق لك ذلك، يمكنك إلقاء نظرة على رمز الحلّ الخاص بدرس البرمجة. يمكنك الاطّلاع على مستودع github أو تنزيل الرمز البرمجي هنا:

تنزيل رمز المصدر

فكّ ضغط ملف ZIP الذي تم تنزيله. سيؤدي ذلك إلى فك حزمة مجلد جذر باسم android-room-with-a-view-kotlin يحتوي على التطبيق الكامل.