اتاق اندروید با منظره - Kotlin

هدف از Architecture Components ارائه راهنمایی در مورد معماری برنامه، با کتابخانه هایی برای کارهای رایج مانند مدیریت چرخه حیات و تداوم داده است. اجزای معماری به شما کمک می‌کنند تا برنامه‌تان را به گونه‌ای طراحی کنید که قوی، قابل آزمایش و قابل نگهداری با کد دیگ بخار کمتر باشد. کتابخانه های اجزای معماری بخشی از Android Jetpack هستند.

این نسخه Kotlin از Codelab است. نسخه به زبان برنامه نویسی جاوا را می توانید در اینجا بیابید .

اگر در حین کار با این کد با مشکلاتی (اشکالات کد، خطاهای دستوری، عبارت نامشخص و غیره) مواجه شدید، لطفاً مشکل را از طریق پیوند گزارش یک اشتباه در گوشه سمت چپ پایین صفحه کد گزارش کنید.

پیش نیازها

شما باید با Kotlin، مفاهیم طراحی شی گرا و اصول توسعه اندروید آشنا باشید، به ویژه:

همچنین به آشنایی با الگوهای معماری نرم افزاری که داده ها را از رابط کاربری جدا می کند، مانند MVP یا MVC کمک می کند. این Codelab معماری تعریف شده در راهنمای معماری برنامه را پیاده سازی می کند.

این Codelab بر روی اجزای معماری اندروید متمرکز شده است. مفاهیم و کدهای خارج از موضوع برای شما ارائه شده است تا به سادگی کپی و پیست کنید.

اگر با Kotlin آشنایی ندارید، نسخه ای از این کد لبه به زبان برنامه نویسی جاوا در اینجا ارائه شده است.

کاری که خواهی کرد

در این کد لبه، یاد خواهید گرفت که چگونه با استفاده از Architecture Components Room، ViewModel و LiveData یک اپلیکیشن طراحی و بسازید و اپلیکیشنی بسازید که کارهای زیر را انجام دهد:

  • معماری توصیه شده ما را با استفاده از اجزای معماری Android پیاده سازی می کند.
  • برای دریافت و ذخیره داده ها با پایگاه داده کار می کند و پایگاه داده را با چند کلمه از قبل پر می کند.
  • تمام کلمات موجود در RecyclerView را در MainActivity نمایش می دهد.
  • وقتی کاربر روی دکمه + ضربه می‌زند، دومین فعالیت را باز می‌کند. هنگامی که کاربر کلمه ای را وارد می کند، کلمه را به پایگاه داده و لیست اضافه می کند.

این برنامه بدون حاشیه است، اما به اندازه کافی پیچیده است که می توانید از آن به عنوان یک الگو برای ساختن استفاده کنید. در اینجا یک پیش نمایش است:

آنچه شما نیاز دارید

  • Android Studio نسخه 3.0 یا بالاتر و آگاهی از نحوه استفاده از آن. اطمینان حاصل کنید که Android Studio و همچنین SDK و Gradle شما به روز شده است.
  • یک دستگاه اندروید یا شبیه ساز.

این لبه کد تمام کدهایی را که برای ساختن برنامه کامل نیاز دارید را ارائه می کند.

مراحل زیادی برای استفاده از اجزای معماری و اجرای معماری پیشنهادی وجود دارد. مهمترین چیز این است که یک مدل ذهنی از آنچه در حال وقوع است ایجاد کنید، درک اینکه چگونه قطعات با هم قرار می گیرند و چگونه جریان داده می شود. در حین کار با این کد، فقط کد را کپی و جایگذاری نکنید، بلکه سعی کنید شروع به ایجاد این درک درونی کنید.

برای معرفی اصطلاحات، در اینجا معرفی کوتاهی از اجزای معماری و نحوه کار آنها با یکدیگر ارائه شده است. توجه داشته باشید که این کد لبه روی زیرمجموعه ای از مؤلفه ها، یعنی LiveData، ViewModel و Room تمرکز می کند. هر جزء با استفاده از آن بیشتر توضیح داده می شود.

این نمودار یک شکل اساسی از معماری را نشان می دهد:

Entity : کلاس Annotated که یک جدول پایگاه داده را هنگام کار با Room توصیف می کند.

پایگاه داده SQLite: در حافظه دستگاه. کتابخانه تداوم اتاق این پایگاه داده را برای شما ایجاد و نگهداری می کند.

DAO : شی دسترسی به داده. نگاشت پرس و جوهای SQL به توابع. وقتی از DAO استفاده می‌کنید، متدها را فراخوانی می‌کنید و Room به بقیه رسیدگی می‌کند.

پایگاه داده اتاق : کار پایگاه داده را ساده می کند و به عنوان یک نقطه دسترسی به پایگاه داده زیرین SQLite عمل می کند ( SQLiteOpenHelper) . پایگاه داده اتاق از DAO برای ارسال پرس و جو به پایگاه داده SQLite استفاده می کند.

Repository: کلاسی که ایجاد می کنید و در درجه اول برای مدیریت چندین منبع داده استفاده می شود.

ViewModel : به عنوان یک مرکز ارتباطی بین مخزن (داده) و UI عمل می کند. رابط کاربری دیگر نیازی به نگرانی در مورد منشا داده ها ندارد. نمونه های ViewModel از Activity/Fragment recreation جان سالم به در می برند.

LiveData : یک کلاس دارنده داده که قابل مشاهده است. همیشه آخرین نسخه داده ها را در حافظه پنهان نگه می دارد و در صورت تغییر داده ها به ناظران خود اطلاع می دهد. LiveData از چرخه حیات آگاه است. اجزای UI فقط داده های مربوطه را مشاهده می کنند و مشاهده را متوقف نمی کنند یا از سر نمی گیرند. LiveData به طور خودکار همه اینها را مدیریت می کند زیرا از تغییرات وضعیت چرخه حیات مربوطه هنگام مشاهده آگاه است.

نمای کلی معماری RoomWordSample

نمودار زیر تمام قطعات برنامه را نشان می دهد. هر یک از کادرهای محصور (به جز پایگاه داده SQLite) نشان دهنده کلاسی است که شما ایجاد خواهید کرد.

  1. Android Studio را باز کنید و روی Start a new Android Studio کلیک کنید.
  2. در پنجره Create New Project، Empty Activity را انتخاب کرده و Next را بزنید.
  3. در صفحه بعدی، نام برنامه را RoomWordSample بگذارید و روی Finish کلیک کنید.

در مرحله بعد، باید کتابخانه های مؤلفه را به فایل های Gradle خود اضافه کنید.

  1. در اندروید استودیو، روی تب Projects کلیک کنید و پوشه Gradle Scripts را باز کنید.

build.gradle باز کنید ( ماژول: برنامه ).

  1. با اضافه کردن افزونه‌های دیگر تعریف‌شده در بالای build.gradle ( Module: app )، پلاگین kapt Annotation Processor Kotlin را اعمال کنید.
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 به شما امکان می دهد جداول را از طریق یک Entity ایجاد کنید. بیایید این کار را اکنون انجام دهیم.

  1. یک فایل کلاس Kotlin جدید به نام Word ایجاد کنید که حاوی کلاس داده Word است.
    این کلاس Entity (که نشان دهنده جدول SQLite است) را برای کلمات شما توصیف می کند. هر ویژگی در کلاس نشان دهنده یک ستون در جدول است. Room در نهایت از این ویژگی ها برای ایجاد جدول و نمونه سازی اشیاء از ردیف های پایگاه داده استفاده می کند.

این هم کد:

data class Word(val word: String)

برای معنی دار کردن کلاس Word برای پایگاه داده اتاق، باید آن را حاشیه نویسی کنید. حاشیه نویسی نشان می دهد که هر بخش از این کلاس چگونه با یک ورودی در پایگاه داده ارتباط دارد. 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 است.

می‌توانید فهرست کاملی از حاشیه‌نویسی‌ها را در مرجع خلاصه بسته اتاق پیدا کنید.

DAO چیست؟

در DAO (شیء دسترسی به داده)، کوئری های SQL را مشخص می کنید و آنها را با فراخوانی متد مرتبط می کنید. کامپایلر SQL را بررسی می کند و پرس و جوهایی را از حاشیه نویسی های راحت برای جستارهای رایج، مانند @Insert می کند. Room از DAO برای ایجاد یک API تمیز برای کد شما استفاده می کند.

DAO باید یک کلاس رابط یا انتزاعی باشد.

به طور پیش فرض، تمام پرس و جوها باید در یک رشته جداگانه اجرا شوند.

اتاق دارای پشتوانه روال‌ها است، که به درخواست‌های شما اجازه می‌دهد با اصلاح‌کننده 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 آن را به عنوان یک کلاس DAO برای Room شناسایی می کند.
  • suspend fun insert(word: Word) : یک تابع suspend را برای درج یک کلمه اعلام می کند.
  • حاشیه نویسی @Insert یک حاشیه نویسی ویژه روش DAO است که در آن نیازی به ارائه SQL نیست! (همچنین @Delete و @Update برای حذف و به‌روزرسانی ردیف‌ها وجود دارد، اما شما از آنها در این برنامه استفاده نمی‌کنید.)
  • onConflict = OnConflictStrategy.IGNORE : استراتژی onConflict انتخاب شده در صورتی که کلمه جدیدی دقیقاً مشابه کلمه قبلی در لیست باشد، نادیده می گیرد. برای دانستن بیشتر در مورد راهبردهای درگیری موجود، مستندات را بررسی کنید.
  • suspend fun deleteAll() : یک تابع suspend را برای حذف همه کلمات اعلام می کند.
  • هیچ حاشیه نویسی راحتی برای حذف چندین نهاد وجود ندارد، بنابراین با @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 یک لایه پایگاه داده در بالای پایگاه داده SQLite است.
  • اتاق از وظایف دنیوی که قبلاً با SQLiteOpenHelper انجام می‌دادید، مراقبت می‌کند.
  • Room از DAO برای ارسال پرس و جو به پایگاه داده خود استفاده می کند.
  • به‌طور پیش‌فرض، برای جلوگیری از عملکرد ضعیف رابط کاربری، Room به شما اجازه نمی‌دهد در رشته اصلی درخواست‌هایی صادر کنید. وقتی کوئری‌های اتاق LiveData را برمی‌گردانند، کوئری‌ها به‌طور خودکار به‌صورت ناهمزمان بر روی یک رشته پس‌زمینه اجرا می‌شوند.
  • Room بررسی های زمان کامپایل عبارات SQLite را فراهم می کند.

پایگاه داده اتاق را پیاده سازی کنید

کلاس پایگاه داده اتاق شما باید انتزاعی باشد و RoomDatabase را گسترش دهد. معمولاً برای کل برنامه فقط به یک نمونه از پایگاه داده اتاق نیاز دارید.

حالا یکی بسازیم

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

بیایید از طریق کد عبور کنیم:

  • کلاس پایگاه داده اتاق باید abstract باشد و RoomDatabase را گسترش دهد
  • کلاس را به عنوان پایگاه داده اتاق با @Database حاشیه نویسی می کنید و از پارامترهای annotation برای اعلام موجودیت های متعلق به پایگاه داده و تنظیم شماره نسخه استفاده می کنید. هر موجودیت مربوط به جدولی است که در پایگاه داده ایجاد می شود. انتقال پایگاه داده فراتر از محدوده این Codelab است، بنابراین برای جلوگیری از هشدار ساخت، exportSchema را در اینجا روی false قرار دادیم. در یک برنامه واقعی، باید فهرستی را برای اتاق تنظیم کنید تا از آن برای صادر کردن طرحواره استفاده کنید تا بتوانید طرح فعلی را در سیستم کنترل نسخه خود بررسی کنید.
  • پایگاه داده DAO ها را از طریق یک روش انتزاعی "گیرنده" برای هر @Dao نشان می دهد.
  • ما برای جلوگیری از باز شدن همزمان چندین نمونه از پایگاه داده، یک تک تن، WordRoomDatabase, تعریف کرده ایم.
  • getDatabase تک تن را برمی گرداند. این پایگاه داده را اولین باری که به آن دسترسی پیدا کرد، با استفاده از سازنده پایگاه داده اتاق ایجاد می کند تا یک شی RoomDatabase در زمینه برنامه از کلاس WordRoomDatabase و نام آن را "word_database" .

مخزن چیست؟

یک کلاس مخزن، دسترسی به چندین منبع داده را خلاصه می کند. مخزن بخشی از کتابخانه های اجزای معماری نیست، اما بهترین روش پیشنهادی برای جداسازی کد و معماری است. یک کلاس Repository یک API تمیز برای دسترسی به داده ها به بقیه برنامه ارائه می دهد.

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

یک مخزن پرس و جوها را مدیریت می کند و به شما امکان می دهد از چندین پشتیبان استفاده کنید. در رایج‌ترین مثال، 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 ارائه داده به UI و حفظ تغییرات پیکربندی است. ViewModel به عنوان یک مرکز ارتباطی بین Repository و UI عمل می کند. همچنین می توانید از ViewModel برای اشتراک گذاری داده ها بین قطعات استفاده کنید. ViewModel بخشی از کتابخانه چرخه حیات است .

برای راهنمای مقدماتی این موضوع، به ViewModel Overview یا ViewModels: A Simple Example پست وبلاگ مراجعه کنید.

چرا از ViewModel استفاده کنیم؟

ViewModel داده‌های رابط کاربری برنامه شما را به شیوه‌ای مبتنی بر چرخه حیات نگهداری می‌کند که از تغییرات پیکربندی جان سالم به در می‌برد. جدا کردن داده‌های رابط کاربری برنامه از کلاس‌های Activity و Fragment به شما امکان می‌دهد بهتر از اصل مسئولیت واحد پیروی کنید: فعالیت‌ها و قطعات شما مسئول کشیدن داده‌ها به صفحه هستند، در حالی که ViewModel شما می‌تواند از نگهداری و پردازش تمام داده‌های مورد نیاز برای رابط کاربری مراقبت کند. .

در ViewModel ، از LiveData برای داده های قابل تغییری که UI استفاده می کند یا نمایش می دهد، استفاده کنید. استفاده از LiveData چندین مزیت دارد:

  • می توانید یک ناظر روی داده ها قرار دهید (به جای نظرسنجی برای تغییرات) و فقط آن را به روز کنید
    UI زمانی که داده ها واقعا تغییر می کنند.
  • Repository و UI کاملاً توسط ViewModel از هم جدا شده اند.
  • هیچ تماس پایگاه داده از ViewModel وجود ندارد (همه اینها در مخزن انجام می شود) که باعث می شود کد قابل آزمایش تر باشد.

viewModelScope

در Kotlin، تمام کوروتین ها در یک CoroutineScope اجرا می شوند. یک scope طول عمر کوروتین ها را از طریق کار خود کنترل می کند. هنگامی که کار یک محدوده را لغو می کنید، تمام کارهای انجام شده در آن محدوده را لغو می کند.

کتابخانه AndroidX lifecycle-viewmodel-ktx viewModelScope را به عنوان تابعی از کلاس ViewModel اضافه می کند که به شما امکان می دهد با scope ها کار کنید.

برای کسب اطلاعات بیشتر در مورد کار با کوروتین ها در ViewModel، مرحله 5 استفاده از Kotlin Coroutine ها را در نرم افزار کد برنامه Android یا Easy Coroutines در Android بررسی کنید: viewModelScope blogpost .

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() از UI کپسوله می شود. ما نمی‌خواهیم insert رشته اصلی را مسدود کند، بنابراین یک کوروتین جدید راه‌اندازی می‌کنیم و insert مخزن را فراخوانی می‌کنیم، که یک تابع تعلیق است. همانطور که گفته شد، ViewModel ها بر اساس چرخه زندگی خود دارای یک محدوده کاری به نام viewModelScope هستند که در اینجا از آن استفاده می کنیم.

در مرحله بعد، باید طرح XML را برای لیست و موارد اضافه کنید.

این کد لبه فرض می کند که شما با ایجاد طرح بندی در XML آشنا هستید، بنابراین ما فقط کد را در اختیار شما قرار می دهیم.

با قرار دادن والد 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>

ظاهر FAB شما باید با عملکرد موجود مطابقت داشته باشد، بنابراین ما می خواهیم نماد را با نماد '+' جایگزین کنیم.

ابتدا باید یک دارایی برداری جدید اضافه کنیم:

  1. File > New > Vector Asset را انتخاب کنید.
  2. روی نماد ربات اندروید در قسمت Clip Art: کلیک کنید.
  3. «افزودن» را جستجو کنید و دارایی «+» را انتخاب کنید. روی OK کلیک کنید.
  4. پس از آن، روی Next کلیک کنید.
  5. مسیر نماد را به عنوان main > drawable تأیید کنید و برای افزودن دارایی روی Finish کلیک کنید.
  6. هنوز در layout/activity_main.xml ، FAB را به‌روزرسانی کنید تا کشش جدید را نیز در بر گیرد:
<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() را لغو می کنید. از آنجایی که نمی‌توانید عملیات پایگاه داده اتاق را روی رشته UI انجام دهید، onOpen() یک coroutine در IO Dispatcher راه‌اندازی می‌کند.

برای راه اندازی یک کوروتین به یک CoroutineScope نیاز داریم. متد getDatabase از کلاس WordRoomDatabase را به روز کنید تا یک محدوده Coroutine را نیز به عنوان پارامتر دریافت کنید:

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

مقداردهی اولیه بازیابی پایگاه داده را در بلوک init WordViewModel به روز کنید تا محدوده زیر را نیز بگذرانید:

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

در WordRoomDatabase ، ما یک پیاده سازی سفارشی از RoomDatabase.Callback() ایجاد می کنیم که یک CoroutineScope را نیز به عنوان پارامتر سازنده دریافت می کند. سپس، متد onOpen را لغو می کنیم تا پایگاه داده پر شود.

در اینجا کد ایجاد callback در کلاس 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() callback را به دنباله ساخت پایگاه داده اضافه کنید:

.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. روی ماژول برنامه در پنجره Project کلیک کنید.
  2. File > New > Android Resource File را انتخاب کنید
  3. از واجد شرایط، گزینه Dimension را انتخاب کنید
  4. نام فایل را تنظیم کنید: dimens

این منابع ابعاد را در values/dimens.xml :

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

با الگوی Empty Activity یک Activity اندروید خالی جدید ایجاد کنید:

  1. File > New > Activity > Empty Activity را انتخاب کنید
  2. برای نام Activity NewWordActivity را وارد کنید.
  3. بررسی کنید که فعالیت جدید به مانیفست Android اضافه شده باشد.
<activity android:name=".NewWordActivity"></activity>

فایل activity_new_word.xml را در پوشه layout با کد زیر به روز کنید:

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

مرحله آخر اتصال UI به پایگاه داده با ذخیره کلمات جدیدی که کاربر وارد می کند و نمایش محتوای فعلی پایگاه داده کلمه در RecyclerView است.

برای نمایش محتویات فعلی پایگاه داده، ناظری اضافه کنید که LiveData را در ViewModel مشاهده کند.

هر زمان که داده ها تغییر می کنند، onChanged() فراخوانی می شود که setWords() آداپتور را برای به روز رسانی داده های کش آداپتور و بازخوانی لیست نمایش داده شده فراخوانی می کند.

در MainActivity ، یک متغیر عضو برای ViewModel ایجاد کنید:

private lateinit var wordViewModel: WordViewModel

از ViewModelProvider برای مرتبط کردن ViewModel خود با Activity خود استفاده کنید.

هنگامی که Activity شما برای اولین بار شروع می شود، ViewModel ViewModelProviders را ایجاد می کند. وقتی اکتیویتی از بین می‌رود، مثلاً از طریق تغییر پیکربندی، 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) }
})

می‌خواهیم هنگام ضربه زدن روی FAB، NewWordActivity را باز کنیم و پس از بازگشت به MainActivity ، کلمه جدید را در پایگاه داده وارد کنیم یا یک Toast نشان دهیم. برای رسیدن به این هدف، اجازه دهید با تعریف یک کد درخواست شروع کنیم:

private val newWordActivityRequestCode = 1

در MainActivity ، کد onActivityResult() را برای NewWordActivity کنید.

اگر اکتیویتی با RESULT_OK برمی گردد، کلمه برگشتی را با فراخوانی متد insert() RESULT_OK در پایگاه داده وارد 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, زمانی که کاربر روی FAB ضربه می زند، NewWordActivity را شروع کنید. در MainActivity onCreate ، FAB را پیدا کنید و یک 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>> : به روز رسانی خودکار در اجزای UI را امکان پذیر می کند. در MainActivity ، یک Observer وجود دارد که کلمات LiveData را از پایگاه داده مشاهده می کند و زمانی که آنها تغییر می کنند به او اطلاع می دهند.
  • Repository: یک یا چند منبع داده را مدیریت می کند. Repository روش هایی را برای ViewModel برای تعامل با ارائه دهنده داده های اساسی نشان می دهد. در این برنامه، آن باطن یک پایگاه داده اتاق است.
  • Room : یک بسته بندی در اطراف است و یک پایگاه داده SQLite را پیاده سازی می کند. اتاق کارهای زیادی را برای شما انجام می دهد که قبلاً باید خودتان انجام می دادید.
  • DAO: فراخوانی‌های متد را به کوئری‌های پایگاه داده نشان می‌دهد، به طوری که وقتی Repository متدی مانند getAlphabetizedWords() را فراخوانی می‌کند، Room می‌تواند SELECT * from word_table ORDER BY word ASC را اجرا کند.
  • Word : کلاس موجودیتی است که حاوی یک کلمه است.

* Views و ActivitiesFragments ) فقط از طریق ViewModel با داده ها تعامل دارند. به این ترتیب، مهم نیست که داده ها از کجا آمده اند.

جریان داده برای به‌روزرسانی‌های خودکار UI (واسطه کاربر واکنش‌گرا)

به روز رسانی خودکار امکان پذیر است زیرا ما از LiveData استفاده می کنیم. در MainActivity ، یک Observer وجود دارد که کلمات LiveData را از پایگاه داده مشاهده می کند و زمانی که آنها تغییر می کنند به او اطلاع می دهند. هنگامی که تغییری ایجاد می شود، onChange() ناظر اجرا می شود و mWords WordListAdapter روز می کند.

داده ها را می توان مشاهده کرد زیرا LiveData است. و آنچه مشاهده می شود LiveData<List<Word>> است که توسط ویژگی WordViewModel a llWords می شود.

WordViewModel همه چیز را در مورد backend از لایه UI پنهان می کند. روش هایی برای دسترسی به لایه داده ارائه می دهد و LiveData را برمی گرداند تا MainActivity بتواند رابطه مشاهدهگر را تنظیم کند. Views و ActivitiesFragments ) فقط از طریق ViewModel با داده ها تعامل دارند. به این ترتیب، مهم نیست که داده ها از کجا آمده اند.

در این مورد، داده ها از یک Repository می آیند. ViewModel نیازی به دانستن اینکه مخزن با چه چیزی تعامل دارد، ندارد. فقط باید بداند که چگونه با Repository تعامل داشته باشد، که از طریق روش هایی است که توسط Repository در معرض دید قرار می گیرد.

Repository یک یا چند منبع داده را مدیریت می کند. در برنامه WordListSample ، آن باطن یک پایگاه داده اتاق است. Room یک بسته بندی در اطراف است و یک پایگاه داده SQLite را پیاده سازی می کند. اتاق کارهای زیادی را برای شما انجام می دهد که قبلاً باید خودتان انجام می دادید. برای مثال، Room هر کاری را که قبلاً با کلاس SQLiteOpenHelper انجام می دادید، انجام می دهد.

روش DAO maps کوئری های پایگاه داده را فراخوانی می کند، به طوری که وقتی Repository متدی مانند getAllWords() را فراخوانی می کند، Room می تواند SELECT * from word_table ORDER BY word ASC اجرا کند.

از آنجایی که نتیجه برگردانده شده از پرس و جو LiveData مشاهده می شود، هر بار که داده های اتاق تغییر می کند، onChanged() واسط Observer اجرا می شود و UI به روز می شود.

[اختیاری] کد راه حل را دانلود کنید

اگر قبلاً این کار را نکرده‌اید، می‌توانید به کد راه‌حل مربوط به Codelab نگاهی بیندازید. می توانید به مخزن github نگاه کنید یا کد را از اینجا دانلود کنید:

کد منبع را دانلود کنید

فایل فشرده دانلود شده را باز کنید. با این کار یک پوشه ریشه، android-room-with-a-view-kotlin که شامل برنامه کامل است، باز می شود.