שכבת נתונים

שכבת ממשק המשתמש מכילה לוגיקה של ממשק משתמש ומצב שקשורים לממשק המשתמש, אבל שכבת הנתונים שכולל נתוני אפליקציות ולוגיקה עסקית. הלוגיקה העסקית היא מעניקה ערך לאפליקציה — היא מורכבת מכללים עסקיים אמיתיים האופן שבו צריך ליצור, לאחסן ולשנות נתוני אפליקציה.

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

ארכיטקטורת שכבות הנתונים

שכבת הנתונים מורכבת ממאגרים שכל אחד מהם יכול להכיל בין אפס להרבה. מקורות הנתונים. צריך ליצור מחלקה של מאגר נתונים לכל סוג שונה של אילו נתונים מטפלים בהם באפליקציה. לדוגמה, אפשר ליצור MoviesRepository מחלקה לנתונים שקשורים לסרטים, או מחלקה PaymentsRepository לנתונים שקשורים לתשלומים.

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

מחלקות המאגרים אחראים למשימות הבאות:

  • חשיפת נתונים לשאר האפליקציה.
  • ריכוז שינויים בנתונים.
  • פתרון התנגשויות בין כמה מקורות נתונים.
  • מופשטים של מקורות נתונים משאר האפליקציה.
  • שכוללות לוגיקה עסקית.

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

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

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

בהתאם לשיטות המומלצות של החדרת תלות, המאגר לוקח את מקורות הנתונים כיחסי תלות ב-constructor שלו:

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }

חשיפת ממשקי API

מחלקות בשכבת הנתונים בדרך כלל חושפות פונקציות לביצוע יצירה חד-פעמית קריאה, עדכון ומחיקה של שיחות (CRUD) או קבלת הודעות על שינויים בנתונים במהלך בזמן האימון. שכבת הנתונים צריכה לחשוף את הפרטים הבאים בכל אחד מהמקרים הבאים:

  • פעולות חד-פעמיות: שכבת הנתונים צריכה לחשוף פונקציות השעיה Kotlin; ובשפת התכנות Java, שכבת הנתונים צריכה לחשוף פונקציות שמאפשרות קריאה חוזרת כדי להודיע על תוצאת הפעולה סוגי RxJava Single, Maybe או Completable.
  • כדי לקבל התראות על שינויים בנתונים לאורך זמן: שכבת הנתונים צריכה לחשוף flows ב-Kotlin; ובשפת התכנות Java, בשכבת הנתונים הזאת היא לחשוף קריאה חוזרת (callback) שפולטת את הנתונים החדשים, או סוג Observable או Flowable.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

מוסכמות מתן שמות במדריך הזה

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

סוג הנתונים + מאגר.

לדוגמה: NewsRepository, MoviesRepository או PaymentsRepository.

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

סוג הנתונים + סוג המקור + מקור הנתונים.

לסוג הנתונים יש להשתמש במרחוק או במקומי כדי להתייחס לנתונים כלליים יותר, מכיוון עשויים להשתנות. לדוגמה: NewsRemoteDataSource או NewsLocalDataSource. כדי לדייק יותר למקרה שהמקור חשוב, השתמשו סוג המקור. לדוגמה: NewsNetworkDataSource או NewsDiskDataSource.

אל תציינו שם למקור הנתונים על סמך פרטי הטמעה. לדוגמה: UserSharedPreferencesDataSource – כי מאגרים שמשתמשים במקור הנתונים הזה לא אמור לדעת איך הנתונים נשמרים. אם הכלל הזה פועל, אפשר לשנות בהטמעה של מקור הנתונים (למשל, העברה SharedPreferences במסגרת DataStore) בלי להשפיע על בשכבה שקוראת למקור הזה.

כמה רמות של מאגרים

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

לדוגמה, במאגר שמטפל בנתוני אימות של משתמשים, UserRepository, יכול להיות תלוי במאגרים אחרים כמו LoginRepository ועל RegistrationRepository כדי למלא את הדרישות.

בדוגמה, UserRepository תלוי בשני סוגי מאגרים אחרים:
    LoginRepository, תלוי במקורות אחרים של נתוני התחברות וגם
    RegistrationRepository, שתלוי במקורות אחרים של נתוני רישום.
איור 2. תרשים תלות של מאגר שתלוי במאגר אחר מאגרים.

מקור האמת

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

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

יכול להיות שמאגרים שונים באפליקציה שלכם עובדים עם מקורות מידע שונים. עבור למשל, המחלקה LoginRepository עשויה להשתמש במטמון שלה כמקור האמת והמחלקה PaymentsRepository עשויה להשתמש במקור הנתונים של הרשת.

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

הברגה

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

לתשומת ליבכם: רוב מקורות הנתונים כבר מספקים ממשקי API בטוחים העיקריים, כמו השעיה ה-methods של Room - Retrofit או Ktor המאגר שלכם יכול לנצל את ממשקי ה-API האלה כשהם זמינים.

למידע נוסף על שרשורים, אפשר לעיין במדריך לרקע בעיבוד. למשתמשי Kotlin: coroutines הם האפשרות המומלצת. לעיון בקטע ריצה משימות Android בשרשורים ברקע עבור עבור שפת התכנות Java.

מחזור חיים

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

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

אם האחריות של הכיתה חיונית לכל הבקשה, אפשר scope מופע של המחלקה הזו במחלקה Application. אז זה קורה. המכונה עוקבת אחרי מחזור החיים של האפליקציה. לחלופין, אם תצטרכו להשתמש שוב באותו מופע בתהליך מסוים באפליקציה. לדוגמה, את תהליך הרישום או ההתחברות, צריך להקצות למחלקה את ההיקף של המכונה שהוא הבעלים של מחזור החיים של אותו זרם. לדוגמה, אפשר להגדיר היקף RegistrationRepository שמכיל נתונים בזיכרון RegistrationActivity או הניווט בתרשים של הרישום.

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

לייצג מודלים עסקיים

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

לדוגמה, נניח ששרת News API מחזיר לא רק את הכתבה אבל גם עריכת היסטוריה, תגובות משתמשים ומטא נתונים מסוימים:

data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array<ArticleApiModel>,
    val comments: Array<CommentApiModel>,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)

האפליקציה לא זקוקה לכמות גדולה של מידע על המאמר, כי היא מציג את התוכן של הכתבה על המסך, יחד עם מידע בסיסי על המחבר. מומלץ להפריד בין סוגי מודלים מאגרים חושפים רק את הנתונים ששכבות אחרות בהיררכיה נדרשות. לדוגמה, כך אפשר לחתוך את ArticleApiModel מ- הרשת כדי לחשוף מחלקה של מודל Article לדומיין ולממשק המשתמש בשכבות:

data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)

אפשר להפריד בין סיווגי מודלים בדרכים הבאות:

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

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

סוגים של פעולות על נתונים

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

פעולות מוכוונות ממשק משתמש

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

פעולות מוכוונות ממשק משתמש בדרך כלל מופעלות על ידי שכבת ממשק המשתמש, והן מופעלות מחזור החיים של המתקשר - לדוגמה, מחזור החיים של ViewModel. למידע נוסף על הקטע 'בקשת רשת'. פעולה.

פעולות שממוקדות באפליקציות

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

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

תפעול עסקי

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

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

חשיפת שגיאות

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

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

למידע נוסף על שגיאות בקורוטינים, יש לעיין בחריגים ב: קורוטינים לפוסט בבלוג.

משימות נפוצות

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

שליחת בקשת רשת

שליחת בקשת רשת היא אחת מהמשימות הנפוצות ביותר שאפליקציה ל-Android עשויה לבצע או ביצועים. אפליקציית החדשות צריכה להציג למשתמש את החדשות האחרונות שאוחזר מהרשת. לכן, האפליקציה צריכה סיווג של מקור נתונים כדי לנהל פעולות רשת: NewsRemoteDataSource. כדי לחשוף את המידע שאר האפליקציה, מאגר חדש שמטפל בפעולות על נתוני חדשות נוצר: NewsRepository.

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

יצירת מקור הנתונים

מקור הנתונים צריך לחשוף פונקציה שמחזירה את החדשות האחרונות: רשימה מתוך ArticleHeadline מופעים. מקור הנתונים צריך לספק דרך בטוחה עיקרית כדי לקבל את החדשות האחרונות מהרשת. לשם כך, צריך תלויות ב-CoroutineDispatcher או ב-Executor שבהם תרצו להריץ את המשימה.

שליחת בקשת רשת היא שיחת טלפון פשוטה שמטופלת על ידי fetchLatestNews() חדש method:

class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * Fetches the latest news from the network and returns the result.
     * This executes on an IO-optimized thread pool, the function is main-safe.
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        // Move the execution to an IO-optimized thread since the ApiService
        // doesn't support coroutines and makes synchronous requests.
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }

// Makes news-related network synchronous requests.
interface NewsApi {
    fun fetchLatestNews(): List<ArticleHeadline>
}

הממשק NewsApi מסתיר את ההטמעה של לקוח ממשק ה-API של הרשת; זה לא משנה אם הממשק מגובה על ידי תיקון או HttpURLConnection הסתמכות על ממשקים מאפשרים להחליף את הטמעות ה-API באפליקציה שלכם.

יצירת המאגר

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

// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

כדי ללמוד איך להשתמש במחלקה של המאגר ישירות מהשכבה UI, עיינו במאמר מדריך בנושא שכבת ממשק המשתמש.

הטמעת שמירה במטמון של נתונים בזיכרון

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

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

קובצי מטמון

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

שמירת התוצאה של בקשת הרשת במטמון

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

ההטמעה הבאה שומרת במטמון את המידע החדשותי העדכני למשתנה ב- מאגר שמוגן באמצעות Mutex. אם התוצאה של בקשת הרשת תתבצע בהצלחה, הנתונים מוקצים למשתנה latestNews.

class NewsRepository(
  private val newsRemoteDataSource: NewsRemoteDataSource
) {
    // Mutex to make writes to cached values thread-safe.
    private val latestNewsMutex = Mutex()

    // Cache of the latest news got from the network.
    private var latestNews: List<ArticleHeadline> = emptyList()

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        if (refresh || latestNews.isEmpty()) {
            val networkResult = newsRemoteDataSource.fetchLatestNews()
            // Thread-safe write to latestNews
            latestNewsMutex.withLock {
                this.latestNews = networkResult
            }
        }

        return latestNewsMutex.withLock { this.latestNews }
    }
}

הפיכת פעולה לפעולה לזמן ארוך יותר מהמסך

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

כדי לפעול לפי השיטות המומלצות להזרקת תלות, NewsRepository צריך לקבל את ההיקף כפרמטר ב-constructor שלו, במקום ליצור CoroutineScope. כי מאגרים אמורים לעשות את רוב העבודה שרשורי רקע, צריך להגדיר את CoroutineScope עם Dispatchers.Default או באמצעות מאגר שרשורים משלכם.

class NewsRepository(
    ...,
    // This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
    private val externalScope: CoroutineScope
) { ... }

כי NewsRepository מוכן לבצע פעולות שקשורות לאפליקציות באמצעות CoroutineScope חיצוני, צריך לבצע את הקריאה למקור הנתונים ולשמור התוצאה של קורוטין חדשה שהתחילה בהיקף הזה:

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val externalScope: CoroutineScope
) {
    /* ... */

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        return if (refresh) {
            externalScope.async {
                newsRemoteDataSource.fetchLatestNews().also { networkResult ->
                    // Thread-safe write to latestNews.
                    latestNewsMutex.withLock {
                        latestNews = networkResult
                    }
                }
            }.await()
        } else {
            return latestNewsMutex.withLock { this.latestNews }
        } 
    }
}

async משמש להפעלת הקואוטין בהיקף החיצוני. השם של await נקרא על הסם החדש כדי להשעות עד שבקשת הרשת תחזור נשמרת במטמון. אם עד אז המשתמש עדיין נמצא במסך, ואז הם יראו את החדשות האחרונות; אם המשתמש מתרחק מהמסך, await בוטל, אבל הלוגיקה בתוך async ממשיכה לפעול.

לעיון בבלוג הזה לשלוח לקבלת מידע נוסף על דפוסים עבור CoroutineScope.

שמירה ואחזור נתונים מהדיסק

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

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

  • למערכי נתונים גדולים שצריך לשלוח לגביהם שאילתות, צריך תקינות רפרנציאלית, או צריכים עדכונים חלקיים, צריך לשמור את הנתונים במסד הנתונים של Room. באפליקציית חדשות Google למשל, ניתן לשמור את הכתבות או המחברים במסד הנתונים.
  • במערכי נתונים קטנים שצריך לאחזר ולהגדיר רק (לא שאילתות או עודכן חלקית), צריך להשתמש ב-DataStore. בדוגמה של אפליקציית חדשות Google, סמל ניתן לשמור את פורמט התאריך המועדף או העדפות תצוגה אחרות מאגר נתונים.
  • כדי ליצור מקטעי נתונים כמו אובייקט JSON, צריך להשתמש בקובץ.

כפי שצוין בקטע מקור האמת, כל נתונים המקור פועל רק עם מקור אחד ותואם לסוג נתונים ספציפי ( לדוגמה, News, Authors, NewsAndAuthors או UserPreferences). מחלקות שמשתמשים במקור הנתונים לא אמורים לדעת איך הנתונים נשמרים. לדוגמה, במסד הנתונים או בקובץ.

חדר כמקור נתונים

כי על כל מקור נתונים צריך לעבוד רק עם מקור אחד. מקור נתונים מסוג מסוים, מקור נתונים של 'חדר' יקבל אובייקט גישה לנתונים (DAO) או את מסד הנתונים עצמו כפרמטר. לדוגמה, NewsLocalDataSource עשוי לקחת של NewsDao כפרמטר, ו-AuthorsLocalDataSource עשוי לקחת מופע של AuthorsDao.

במקרים מסוימים, אם לא נדרשת לוגיקה נוספת, אפשר להחדיר את DAO ישירות. למאגר, כי ה-DAO הוא ממשק שאפשר להחליף בקלות בבדיקות.

למידע נוסף על עבודה עם ממשקי ה-API של Room, אפשר לעיין בחדר מדריכים.

DataStore כמקור נתונים

DataStore מושלם לאחסון צמדי מפתח/ערך, כמו הגדרות משתמש. דוגמאות לכך יכולות להיות פורמט זמן, העדפות של התראות, והאם להציג או להסתיר פריטים חדשותיים אחרי המשתמש קרא אותם. DataStore יכול גם לאחסן אובייקטים מוקלדים באמצעות פרוטוקול חוצצים (Buffers).

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

לדוגמה, יכול להיות שיש לך NotificationsDataStore שמשמש רק לטיפול העדפות שקשורות להתראות וNewsPreferencesDataStore שרק המערכת תטפל בהעדפות שקשורות למסך החדשות. כך תוכלו לראות את העדכונים טובים יותר, כי הזרימה של newsScreenPreferencesDataStore.data בלבד פולטת כשהעדפה שקשורה למסך הזה משתנה. המשמעות היא גם מחזור החיים של אובייקט יכול להיות קצר יותר כי הוא יכול לשרוד רק מסך החדשות יוצג.

למידע נוסף על עבודה עם ממשקי ה-API של DataStore, ראו מאגר נתונים מדריכים.

קובץ כמקור נתונים

כשעובדים עם אובייקטים גדולים, כמו אובייקט JSON או מפת סיביות (bitmap), לעבוד עם אובייקט File ולטפל במעבר בין שרשורים.

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

תזמון משימות באמצעות WorkManager

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

בעזרת WorkManager, קל יותר לתזמן עבודה אסינכרונית ואמינה, ויכול לטפל באילוץ ניהול. זו הספרייה המומלצת לעבודה קבועה. כדי לבצע את הפונקציה שהמשימה הוגדרה למעלה, Worker נוצרה כיתה: RefreshLatestNewsWorker. משך הקורס הוא NewsRepository בתור תלות, כדי לאחזר את החדשות האחרונות ולשמור אותן במטמון.

class RefreshLatestNewsWorker(
    private val newsRepository: NewsRepository,
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = try {
        newsRepository.refreshLatestNews()
        Result.success()
    } catch (error: Throwable) {
        Result.failure()
    }
}

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

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

private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"

class NewsTasksDataSource(
    private val workManager: WorkManager
) {
    fun fetchNewsPeriodically() {
        val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
            REFRESH_RATE_HOURS, TimeUnit.HOURS
        ).setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
                .setRequiresCharging(true)
                .build()
        )
            .addTag(TAG_FETCH_LATEST_NEWS)

        workManager.enqueueUniquePeriodicWork(
            FETCH_LATEST_NEWS_TASK,
            ExistingPeriodicWorkPolicy.KEEP,
            fetchNewsRequest.build()
        )
    }

    fun cancelFetchingNewsPeriodically() {
        workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
    }
}

סוגי הכיתות האלה נקראים על שם הנתונים שעליהם הם אחראים – לדוגמה, NewsTasksDataSource או PaymentsTasksDataSource. כל המשימות קשורות לסוג מסוים של נתונים צריך להיכלל באותה מחלקה.

אם צריך להפעיל את המשימה בזמן ההפעלה של האפליקציה, מומלץ להפעיל אותה בקשת WorkManager באמצעות App Startup שקוראת למאגר Initializer.

מידע נוסף על עבודה עם ממשקי API של WorkManager זמין במאמר WorkManager מדריכים.

בדיקה

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

בדיקות יחידה (unit testing)

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

בדיקות שילוב

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

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

בתחום הרשתות, יש ספריות פופולריות כמו WireMock או MockWebServer שמאפשרות לזייף קריאות HTTP ו-HTTPS ולוודא שהבקשות בוצעו מה מצופה.

דוגמיות

הדוגמאות הבאות של Google מדגימות את השימוש בשכבת הנתונים. מומלץ לעיין בהם כדי לראות את ההנחיות האלה בפועל: