חדר Android עם נוף – קוטלין

המטרה של רכיבי אדריכלות היא לספק הדרכה על ארכיטקטורת אפליקציות, עם ספריות לביצוע משימות נפוצות כמו ניהול מחזור חיים ועקביות נתונים. רכיבי הארכיטקטורה עוזרים לכם לבנות את האפליקציה בצורה חזקה, ניתנת לבדיקה ובתחזוקה באמצעות קוד פחות קביל. הספריות של רכיבי הארכיטקטורה הן חלק מ-Jetpack.

זו גרסת Kotlin של שיעור הקוד. הגרסה בשפת התכנות של Java זמינה כאן.

אם תיתקלו בבעיות (באגים בקוד, שגיאות דקדוק, ניסוח לא ברור וכו') במהלך העבודה על מעבדת הקוד הזו, דווחו על הבעיה באמצעות הקישור דיווח על שגיאה בפינה הימנית התחתונה של קוד הקוד.

דרישות מוקדמות

חשוב להכיר את קוטלין, קונספטים שמתמקדים באובייקטים ואת העקרונות הבסיסיים של פיתוח ב-Android, במיוחד:

כמו כן, מומלץ להכיר דפוסים של ארכיטקטורה תוכנה שמפרידה בין נתונים לבין ממשק המשתמש, כגון MVP או MVC. שיעור Lab זה מטמיע את הארכיטקטורה המוגדרת במדריך לארכיטקטורת אפליקציות.

שיעור Lab זה מתמקד ברכיבי ארכיטקטורה של Android. קונספטים וקוד לא קשורים ניתנים לך פשוט להעתקה והדבקה.

אם אינכם מכירים את Kotlin, כאן אתם יכולים למצוא גרסה של ה-Codelab הזה בשפת התכנות Java.

מה צריך לעשות

במעבדה זו תלמדו איך לעצב ולבנות אפליקציה באמצעות חדר הרכיבים, ה-ViewModel ו-LiveData בארכיטקטורה, ובנייה של האפליקציה הבאה:

  • מיישמת את הארכיטקטורה המומלצת באמצעות רכיבי הארכיטקטורה של Android.
  • פועל עם מסד נתונים כדי לקבל ולשמור את הנתונים, ומאכלס מראש את מסד הנתונים במילים מסוימות.
  • הצגת כל המילים בRecyclerView בMainActivity.
  • פתיחת פעילות שנייה כאשר המשתמש מקיש על לחצן +. כשהמשתמש מזין מילה, הוא מוסיף אותה למסד הנתונים ולרשימה.

האפליקציה היא פשוטה, אך מורכבת מספיק כדי להשתמש בה כתבנית לבנייה. הנה תצוגה מקדימה:

מה תצטרך להכין

  • Android Studio 3.0 ואילך וידע איך להשתמש בו. מוודאים ש-Android Studio מעודכן, וגם ה-SDK ו-Gradle.
  • מכשיר Android או אמולטור.

Lablab זה מספק את כל הקוד הדרוש לבניית האפליקציה המלאה.

יש הרבה שלבים לשימוש ברכיבי ארכיטקטורה ויישום של הארכיטקטורה המומלצת. הדבר החשוב ביותר הוא ליצור מודל נפשי של מה שקורה, כדי להבין איך החלקים תואמים זה לזה ואיך הנתונים עוברים. תוך כדי עבודה ב-codelab הזה, אין להעתיק ולהדביק את הקוד, אלא לנסות להתחיל לבנות את ההבנה הפנימית הזו.

כדי להציג את המינוח, אנחנו מבואים בקצרה לרכיבי הארכיטקטורה ואיך הם פועלים יחד. שימו לב: שיעור Lab זה מתמקד בקבוצת משנה של הרכיבים, בעיקר LiveData, ViewModel ו-חדר. כל רכיב מכיל הסבר מפורט ככל שמשתמשים בו.

התרשים הזה מראה צורה בסיסית של הארכיטקטורה:

ישות: מחלקה עם הערות המתארת טבלה של מסד נתונים בעת עבודה עם חדר.

מסד נתונים של SQL: באחסון המכשיר. הספרייה של חדר האחסון העקבית יוצרת את מסד הנתונים הזה בשבילך.

DAO: אובייקט גישה לנתונים. מיפוי של שאילתות SQL לפונקציות. כשמשתמשים ב-DAO, מתקשרים לשיטות והחדר מטפל בכל השאר.

מסד נתונים של חדרים: תצוגת מסד נתונים פשוטה יותר, משמשת כנקודת גישה למסד הנתונים של SQLite (מסתירה את SQLiteOpenHelper)). במסד הנתונים של החדר, נעשה שימוש ב-DAO כדי לשלוח שאילתות למסד הנתונים של SQLite.

מאגר: כיתה שאתם משתמשים בה בעיקר כדי לנהל מספר מקורות נתונים.

ViewModel: משמש כמרכז תקשורת בין המאגר (נתונים) לבין ממשק המשתמש. ממשק המשתמש כבר לא צריך לדאוג לגבי מקור הנתונים. מכונות ה-ViewModel משפיעות על פעילויות פנאי/מקטע.

LiveData: סיווג של בעלי הנתונים שאפשר לצפות בו. תמיד שומר/שומר מטמון של הגרסה העדכנית ביותר של הנתונים, ומעדכן את הבודקים מתי הנתונים משתנים. LiveData מודעת למחזור חיים. רכיבי ממשק המשתמש פשוט מעיינים בנתונים רלוונטיים ולא עוצרים או ממשיכים את התצפית. LiveData מנהל את כל התהליך הזה באופן אוטומטי, מפני שהוא מודע לשינויים הרלוונטיים בסטטוס מחזור החיים במהלך הצפייה.

סקירה כללית של ארכיטקטורת RoomWordSample

התרשים הבא מציג את כל חלקי האפליקציה. כל אחת מתיבות הסוגריים (למעט מסד הנתונים של SQLite) מייצגת מחלקה שתיצרו.

  1. פותחים את Android Studio ולוחצים על התחלת פרויקט חדש ב-Android Studio.
  2. בחלון 'יצירת פרויקט חדש', בוחרים באפשרות פעילות ריקה ולוחצים על הבא.
  3. במסך הבא, נותנים שם לאפליקציה RoomWordSample ולוחצים על סיום.

בשלב הבא, צריך להוסיף את ספריות הרכיבים לקובצי Gradle.

  1. ב-Android Studio, לוחצים על הכרטיסייה 'פרויקטים' ומרחיבים את התיקייה Gradle Scripts.

פותחים את build.gradle (מודול: אפליקציה).

  1. אפשר להחיל את הפלאגין kapt
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'
}

הנתונים של האפליקציה הזו הם מילים ויהיה צורך בטבלה פשוטה כדי לשמור את הערכים הבאים:

חדר מאפשר ליצור טבלאות באמצעות ישות. קדימה, עושים את זה עכשיו.

  1. יצירת קובץ חדש של מחלקת Kotlin בשם Word שמכיל את סיווג הנתונים של Word.
    הכיתה הזו מתארת את הישות (שמייצגת את טבלת SQLite) עבור המילים שלך. כל נכס בכיתה מייצג עמודה בטבלה. החדר הזה ישתמש בסופו של דבר במאפיינים האלה כדי ליצור את הטבלה וליצור אובייקטים משורות במסד הנתונים.

הנה הקוד:

data class Word(val word: String)

כדי שסיווג Word יהיה משמעותי במסד הנתונים של החדר, צריך להוסיף לו הערות. ההערות מציינות איך כל חלק בכיתה הזה מתייחס לרשומה במסד הנתונים. החדר משתמש במידע הזה כדי ליצור קוד.

אם הקלדתם את ההערות בעצמכם (במקום להדביק אותן), מערכת Android Studio תייבא באופן אוטומטי את סוגי ההערות.

  1. יש לעדכן את הכיתה ב-Word באמצעות הערות, כפי שמוצג בקוד הזה:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

בואו נראה מה ההערות האלה עושות:

  • @Entity(tableName = "word_table")
    כל מחלקה של @Entity מייצגת טבלת SQLite. סימון הצהרת הכיתה בתור ישות&#39. אפשר לציין את שם הטבלה אם רוצים שהיא תהיה שונה משם הכיתה. פעולה זו נותנים שם לטבלה "word_table".
  • @PrimaryKey
    לכל ישות נדרש מפתח ראשי. כדי לפשט את הדברים, כל מילה משמשת כמפתח ראשי משלה.
  • @ColumnInfo(name = "word")
    הפרמטר הזה מציין את שם העמודה בטבלה אם הוא רוצה שהוא יהיה שונה משם של משתנה החברים בקבוצה. פעולה זו נותנים לעמודה את העמודה "word".
  • כל נכס שמאוחסן במסד הנתונים צריך להיות גלוי לציבור, שהוא ברירת המחדל של Kotlin.

ניתן למצוא רשימה מלאה של הערות בהפניות לסיכום חבילת החדרים.

מה זה DAO?

ב-DAO (אובייקט גישה לנתונים), עליכם לציין שאילתות SQL ולשייך אותן לקריאות השיטה. המהדר בודק את ה-SQL ויוצר שאילתות מהערות נוחות בתגובה לשאילתות נפוצות, כמו @Insert. בחדר נעשה שימוש ב-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 לחדר.
  • suspend fun insert(word: Word) : הצהרה על פונקציית השעיה כדי להוסיף מילה אחת.
  • ההערת @Insert היא הערה מיוחדת בשיטת DAO, שבה אין צורך לספק SQL! (יש גם הערות @Delete ו-@Update למחיקה ולעדכון של שורות, אבל לא נעשה בהן שימוש באפליקציה הזו.)
  • onConflict = OnConflictStrategy.IGNORE: השיטה שנבחרה להתנגשויות מתעלמת מהמילה החדשה אם היא זהה בדיוק למילה שכבר מופיעה ברשימה. למידע נוסף על האסטרטגיות הזמינות להתנגשות, כדאי לעיין בתיעוד.
  • 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 בתיאור השיטה, והחדר יפיק את כל הקוד הדרוש לעדכון ה-LiveData כשמסד הנתונים יעודכן.

ב-WordDao, משנים את החתימה של השיטה getAlphabetizedWords() כך שה-List<Word> המוחזר ייתווסף ב-LiveData.

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

מאוחר יותר במעבדה הזו, ב-MainActivity יתבצע מעקב אחר שינויים בנתונים באמצעות Observer.

מהו מסד נתונים של חדרים?

  • חדר הוא שכבה של מסד נתונים מעל מסד נתונים של SQLite.
  • החדר מטפל במשימות יומיומיות שבהן השתמשת לצורך SQLiteOpenHelper.
  • חדר משתמש ב-DAO כדי לשלוח שאילתות למסד הנתונים שלו.
  • כברירת מחדל, כדי למנוע ביצועים גרועים של ממשק המשתמש, החדר לא מאפשר לכם לשלוח שאילתות בשרשור הראשי. כאשר שאילתות חדרים מחזירות LiveData, השאילתות רצות באופן אסינכרוני על שרשור רקע באופן אוטומטי.
  • החדר מספק בדיקות בזמן הידור של הצהרות 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, ומשתמשים בפרמטרים של ההערה כדי להצהיר על הישויות ששייכות במסד הנתונים ולהגדיר את מספר הגרסה. כל ישות תואמת לטבלה שתיווצר במסד הנתונים. העברות של מסדי נתונים חורגות מההיקף של קוד ה-lab הזה, ולכן הגדרנו את exportSchema כ-false כאן כדי להימנע מאזהרת build. באפליקציה אמיתית, מומלץ להגדיר ספרייה עבור חדר לשימוש לייצוא הסכימה, כדי שתוכל לבדוק את הסכימה הנוכחית במערכת בקרת הגרסאות.
  • מסד הנתונים מציג DAOs באמצעות שיטה מופשטת "getter" לכל @Dao.
  • הגדרנו singleton, WordRoomDatabase, כדי למנוע מצב שבו מספר מופעים של מסד הנתונים ייפתחו בו-זמנית.
  • getDatabase מחזירה את הסינגלטון. המערכת תיצור את מסד הנתונים בפעם הראשונה שייגשו אליו, באמצעות הכלי ליצירת מסד נתונים של חדר &#39. כדי ליצור אובייקט RoomDatabase בהקשר של האפליקציה, מהכיתה WordRoomDatabase ולתת לו שם "word_database".

מהו מאגר נתונים?

סיווג מאגר מבטל את הגישה למספר מקורות נתונים. המאגר אינו חלק מהספריות של רכיבי הארכיטקטורה, אבל הוא שיטה מומלצת להפרדת קוד ולאדריכלות. מחלקת Repository מספקת ממשק API נקי לגישה לנתונים בשאר האפליקציה.

למה כדאי להשתמש במאגר?

Repository מנהל שאילתות ומאפשרת לך להשתמש בקצה העורפי. בדוגמה הנפוצה ביותר, ה-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 מ'חדר'. אנחנו יכולים לעשות זאת על סמך האופן שבו הגדרנו את השיטה getAlphabetizedWords כך שתחזיר את LiveData בשלב ה&מירכאות; ה-LiveData; החדר יבצע את כל השאילתות בשרשור נפרד. לאחר מכן, LiveData ידווח לצופה בשרשור הראשי כשהנתונים ישתנו.
  • המתאם suspend מציין בפני המהדר שצריך לקרוא ל-Coroutine או מפונקציית השעיה אחרת.

מהו מודל מודל?

התפקיד של ViewModel&#39 הוא לספק נתונים לממשק המשתמש ולשרוד שינויים בהגדרות. ViewModel משמש כמרכז תקשורת בין המאגר לבין ממשק המשתמש. ניתן גם להשתמש ב-ViewModel כדי לשתף נתונים בין מקטעים. ה-ViewModel הוא חלק מספריית מחזור החיים.

למדריך מבוא לנושא הזה, עיינו בViewModel Overview או בפוסט בבלוג ViewModels: Simple Example.

למה כדאי להשתמש ב-ViewModel?

ViewModel שומר את נתוני ממשק המשתמש של האפליקציה שלך באופן מחזורי השומר את הגדרות התצורה. הפרדה בין נתוני ממשק המשתמש של האפליקציה שלך לכיתות ב-Activity ול-Fragment מאפשרת לך לפעול טוב יותר לפי עיקרון האחריות היחיד: הפעילויות והמקטעים שלך אחראיים לשליפת הנתונים על המסך, בעוד ש-ViewModel יכול לטפל בכל הנתונים הדרושים לממשק המשתמש ולעבד אותם.

בViewModel, יש להשתמש ב-LiveData להצגת נתונים שניתן לשנות בממשק המשתמש. יש כמה יתרונות לשימוש ב-LiveData:

  • אפשר להוסיף צופה לנתונים (במקום לערוך סקרים) ולעדכן את ממשק המשתמש
    רק כשהנתונים משתנים בפועל.
  • ה-Repository וה-UI מופרדים לחלוטין באמצעות ViewModel.
  • אין קריאות למסד הנתונים של ViewModel (כולן מטופלות במאגר), כך שהקוד ניתן לבדיקה יותר.

viewModelScope

בקוטלין, כל השגרות מתבצעות ב-CoroutineScope. היקף מסוים קובע את משך החיים של קוראוטים באמצעות העבודה שלו. כשמבטלים את העבודה בהיקף מסוים, כל הביטולים של קורסים שהתחילו באותו היקף מבוטלים.

ספריית AndroidX lifecycle-viewmodel-ktx מוסיפה viewModelScope כפונקציית הרחבה של המחלקה ViewModel, ומאפשרת לך לעבוד עם היקפים.

כדי לקבל מידע נוסף על עבודה עם שגרתיים ב-ViewModel, עיינו בשלב 5 של קוד הקוד של השימוש ב-Cotlin Coroutines באפליקציית Android או ב-Easy Coroutines ב-Android: viewModelScope.

יישום מודל התצוגה

יש ליצור קובץ מחלקה של 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, הפעיל את allWordsLiveData באמצעות המאגר.
  • נוצרה שיטת wrapper insert() שמגדירה את שיטת insert()המאגר. באופן כזה, היישום של insert() מקיף את ממשק המשתמש. אנחנו לא רוצים להוסיף את הבלוק הראשי כדי לחסום את השרשור הראשי, לכן אנחנו משיקים שגרה חדשה וקוראים להכנסת המאגר, שהיא פונקציית השעיה. כפי שצוין, ל-Viewmodels יש היקף קורטהי בהתאם למחזור החיים שלהם שנקרא viewModelScope, שבו אנחנו משתמשים כאן.

בשלב הבא, צריך להוסיף את פריסת ה-XML עבור הרשימה והפריטים.

ערכת ה-Codelab הזו מבוססת על ההנחה שאתם יוצרים פריסות ב-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' צריך להיות תואם לפעולה הזמינה, לכן נבקש להחליף את הסמל בסמל &&39;+'

תחילה, אנחנו צריכים להוסיף נכס וקטור חדש:

  1. בוחרים באפשרות File > New > Vector Asset.
  2. לוחצים על סמל הרובוט של Android בשדה קליפ ארט: בשדה
    .
  3. מחפשים את &"add" ובוחרים את הנכס &&33;+' לוחצים על אישור.
  4. לאחר מכן, לוחצים על הבא.
  5. מאשרים את נתיב הסמלים בתור main > drawable ולוחצים על סיום כדי להוסיף את הנכס.
  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. בשיעור ה-Lab הזה אנחנו יודעים ש-RecyclerView , RecyclerView.LayoutManager , RecyclerView.ViewHolder ו-RecyclerView.Adapter עובדים.

לתשומת ליבכם, המשתנה words במתאם שומר את הנתונים במטמון. במשימה הבאה, מוסיפים את הקוד שמעדכן את הנתונים באופן אוטומטי.

יצירת קובץ class 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(). אי אפשר לבצע פעולות במסד הנתונים של החדר בשרשור של ממשק המשתמש. לכן, onOpen() מפעילה קובץ Coroutin ב-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. לוחצים על מודול האפליקציה בחלון Project.
  2. בוחרים באפשרות File > New > Android Resource File.
  3. בקטע 'פרסים זמינים', בוחרים באפשרות מאפיין
  4. הגדרת שם הקובץ: מעומעם

הוסיפי את משאבי המאפיינים האלה ב-values/dimens.xml:

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

צריך ליצור מכשיר Android Activity חדש ריק עם התבנית 'פעילות ריקה':

  1. בוחרים באפשרות > חדש > פעילות > פעילות ריקה
  2. יש להזין NewWordActivity עבור שם הפעילות.
  3. מוודאים שהפעילות החדשה נוספה למניפסט של Android.
<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(), יש להוסיף צופה לנכס LiveDataWords מ-WordViewModel.

שיטת onChanged() (שיטת ברירת המחדל עבור למבדה שלנו) מופעלת כאשר הנתונים שנצפו משתנים והפעילות פועלת בחזית:

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

אנחנו רוצים לפתוח את NewWordActivity כשמקישים על FAB, וכשאנחנו חוזרים ב-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 כשהמשתמש מקיש על FAB. ב-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<List<Word>>: מאפשר את העדכונים האוטומטיים ברכיבי ממשק המשתמש. בMainActivity יש Observer שבודק את המילים LiveData ממסד הנתונים, ומודיע לו כשהוא משתנה.
  • Repository: מנהל מקור נתונים אחד או יותר. השדה Repository חושף שיטות ל-ViewModel לביצוע אינטראקציה עם ספק הנתונים הבסיסי. באפליקציה הזו, הקצה העורפי הזה הוא מסד נתונים של חדרים.
  • Room: wrapper מסביב ומטמיע מסד נתונים של SQLite. היה הרבה עבודה בעבר.
  • DAO: מיפוי של שיטות קריאה לשאילתות על מסד נתונים, כך שכאשר המאגר קורא שיטה כמו getAlphabetizedWords(), החדר יכול לבצע SELECT * from word_table ORDER BY word ASC.
  • Word: היא סיווג הישות שמכיל מילה אחת.

* Views ו-Activities (וגם Fragments) מקיימים אינטראקציה עם הנתונים רק דרך ViewModel. לכן אין משמעות למקור הנתונים.

זרם נתונים לעדכונים אוטומטיים של ממשק משתמש (ממשק משתמש רספונסיבי)

העדכון האוטומטי אפשרי כי אנחנו משתמשים ב-LiveData. בMainActivity יש Observer שבודק את המילים LiveData ממסד הנתונים, ומודיע לו כשהוא משתנה. כשיש שינוי, שיטת התצפית של onChange() מופעלת ומעדכנים את mWords בWordListAdapter.

ניתן לצפות בנתונים מפני שהם LiveData. מה שנצפה הוא LiveData<List<Word>> שמוחזר מנכס WordViewModel מסוג llWords.

ה-WordViewModel מסתיר את כל הקצה העורפי משכבת ממשק המשתמש. היא מספקת שיטות לגישה לשכבת הנתונים, והיא מחזירה LiveData כדי שמערכת MainActivity תוכל להגדיר את הקשר עם הצופה. Views ו-Activities (ו-Fragments) מקיימים אינטראקציה עם הנתונים רק דרך ViewModel. לכן אין משמעות למקור הנתונים.

במקרה הזה, הנתונים מגיעים מ-Repository. ה-ViewModel לא צריך לדעת מה האינטראקציה של המאגר. צריך פשוט לדעת איך לבצע אינטראקציה עם Repository. אפשר להשתמש בשיטה הזו בRepository.

המאגר מנהל מקור נתונים אחד או יותר. באפליקציה WordListSample, הקצה העורפי הזה הוא מסד נתונים של חדרים. החדר הוא עוטף אותו ומטמיע מסד נתונים של SQLite. היה הרבה עבודה בעבר. לדוגמה, החדר כולל את כל מה שעשיתם עם כיתה אחת (SQLiteOpenHelper).

שיטת DAO ממפה שיחות בשיטה של שאילתות על מסד נתונים, כך שכאשר המאגר קורא שיטה כמו getAllWords(), החדר יכול לבצע SELECT * from word_table ORDER BY word ASC.

התוצאה שמתקבלת מהשאילתה נקלטת ב-LiveData, בכל פעם שהנתונים בחדר משתנים, השיטה onChanged() של ה-Observer מופעלת וממשק המשתמש מתעדכן.

[אופציונלי] הורדת קוד הפתרון

אם עדיין לא עשיתם זאת, תוכלו לעיין בקוד הפתרון של שיעור ה-Lab. כאן אפשר לעיין במאגר Gitub או להוריד את הקוד:

להורדת קוד המקור

פותחים את הקובץ הדחוס. הפעולה הזו תשחרר תיקיית שורש, android-room-with-a-view-kotlin, שמכילה את האפליקציה המלאה.