פיתוח אפליקציה שמתמקדת באופליין

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

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

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

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

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

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

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

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

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

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

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

בניית מודלים של נתונים באפליקציה שנותנת עדיפות לאופליין

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

  • מקור הנתונים המקומי
  • מקור הנתונים של הרשת
שכבת נתונים שמתמקדת באופליין מורכבת גם ממקורות נתונים מקומיים וגם ממקורות נתונים ברשת
איור 1: מאגר שמתמקד באופליין

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

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

  • מקורות נתונים מובְנים, כמו מסדי נתונים רלציוניים כמו room.
  • מקורות נתונים לא מובנים. לדוגמה, אגירת נתונים של פרוטוקולים עם Datastore.
  • קבצים פשוטים

מקור הנתונים של הרשת

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

חשיפת משאבים

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

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

data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/

הפרטים של AuthorEntity ושל NetworkAuthor כוללים:

/**
 * Network representation of [Author]
 */
@Serializable
data class NetworkAuthor(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

/**
 * Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
 * It has a many-to-many relationship with both entities
 */
@Entity(tableName = "authors")
data class AuthorEntity(
    @PrimaryKey
    val id: String,
    val name: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val twitter: String,
    @ColumnInfo(name = "medium_page", defaultValue = "")
    val mediumPage: String,
    @ColumnInfo(defaultValue = "")
    val bio: String,
)

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

/**
 * External data layer representation of a "Now in Android" Author
 */
data class Author(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

לאחר מכן מודל הרשת יכול להגדיר שיטת תוסף כדי להמיר אותה ובמודל המקומי יש מודל מקומי כדי להמיר אותו כמו בדוגמה הבאה:

/**
 * Converts the network model to the local model for persisting
 * by the local data source
 */
fun NetworkAuthor.asEntity() = AuthorEntity(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

/**
 * Converts the local model to the external model for use
 * by layers external to the data layer
 */
fun AuthorEntity.asExternalModel() = Author(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

קריאה

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

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

class OfflineFirstTopicsRepository(
    private val topicDao: TopicDao,
    private val network: NiaNetworkDataSource,
) : TopicsRepository {

    override fun getTopicsStream(): Flow<List<Topic>> =
        topicDao.getTopicEntitiesStream()
            .map { it.map(TopicEntity::asExternalModel) }
}

אסטרטגיות לטיפול בשגיאות

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

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

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

אופן השימוש באופרטור catch ב-ViewModel:

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
   private val authorId: String = ...

   // Observe author information
    private val authorStream: Flow<Author> =
        authorsRepository.getAuthorStream(
            id = authorId
        )
        .catch { emit(Author.empty()) }
}

מקור נתוני הרשת

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

השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

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

קריאת נתונים עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)
איור 2: קריאת נתונים עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

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

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

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

קריאת נתונים באמצעות מוניטורי רשת ותורי רשת
איור 3: קריאת תורים באמצעות ניטור רשת

כתיבה

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

interface UserDataRepository {
    /**
     * Updates the bookmarked status for a news resource
     */
    suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

בקטע הקוד שלמעלה, ה-API האסינכרוני שנבחר הוא Coroutines בתור השיטה שצוינה למעלה מושעה.

כתיבת אסטרטגיות

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

כתיבה אונליין בלבד

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

כתיבה אונליין בלבד
איור 4: אפשרות כתיבה בלבד באינטרנט

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

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

כתיבה בתור

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

כתיבת תורים עם ניסיונות חוזרים
איור 5: כתיבת תורים עם ניסיונות חוזרים

כדאי להשתמש בגישה הזאת אם:

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

תרחישים לדוגמה בגישה הזו כוללים אירועים של ניתוח נתונים ורישום ביומן.

כתיבה מדורגת

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

כתיבה מדורגת באמצעות ניטור רשת
איור 6: כתיבה מדורגת

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

סנכרון ופתרון סכסוכים

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

  • סנכרון מבוסס משיכה
  • סנכרון מבוסס דחיפה

סנכרון מבוסס משיכה

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

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

סנכרון מבוסס שליפה
איור 7: סנכרון מבוסס משיכה: מכשיר א' ניגש למשאבים של מסכים א' ו-ב' בלבד, אבל מכשיר ב' ניגש למשאבים רק במסכים ב', ג' ו-ד'
.

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

class FeedRepository(...) {

    fun feedPagingSource(): PagingSource<FeedItem> { ... }
}

class FeedViewModel(
    private val repository: FeedRepository
) : ViewModel() {
    private val pager = Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = FeedRemoteMediator(...),
        pagingSourceFactory = feedRepository::feedPagingSource
    )

    val feedPagingData = pager.flow
}

סיכום היתרונות והחסרונות של סנכרון מבוסס משיכה בטבלה הבאה:

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

סנכרון מבוסס דחיפה

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

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

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

class UserDataRepository(...) {

    suspend fun synchronize() {
        val userData = networkDataSource.fetchUserData()
        localDataSource.saveUserData(userData)
    }
}

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

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

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

סנכרון היברידי

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

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

יישוב מחלוקות

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

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

הכתיבה האחרונה ניצחה

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

הכתיבה האחרונה זוכה בפתרון המחלוקת
איור 9: "הכתיבה האחרונה היא המנצחת" מקור האמת של הנתונים נקבע על ידי היישות האחרונה כדי לכתוב נתונים

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

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

באסטרטגיות הקריאה והכתיבה שצוינו למעלה, היו כלי שירות:

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

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

  1. הגדר את פעולת סנכרון הקריאה בתור כדי לוודא שיש התאמה בין במקור נתונים מקומי ובמקור הנתונים של הרשת.
  2. יש לנקות את תור סנכרון הקריאה ולהתחיל בסנכרון כשהאפליקציה באינטרנט.
  3. ביצוע קריאה ממקור הנתונים של הרשת באמצעות השהיה מעריכית לפני ניסיון חוזר (exponential backoff).
  4. לשמור על תוצאות הקריאה במקור הנתונים המקומי, תוך פתרון עלולים להתרחש.
  5. חושפים את הנתונים ממקור הנתונים המקומי עבור שכבות אחרות של האפליקציה לצרוך.

הדבר שלמעלה מתואר בתרשים הבא:

סנכרון נתונים באפליקציה &#39;עכשיו&#39; ל-Android
איור 10: סנכרון נתונים באפליקציה 'עכשיו ב-Android'

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

class SyncInitializer : Initializer<Sync> {
   override fun create(context: Context): Sync {
       WorkManager.getInstance(context).apply {
           // Queue sync on app startup and ensure only one
           // sync worker runs at any time
           enqueueUniqueWork(
               SyncWorkName,
               ExistingWorkPolicy.KEEP,
               SyncWorker.startUpSyncWork()
           )
       }
       return Sync
   }
}

כאשר SyncWorker.startupSyncWork() מוגדר כך:


/**
 Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
 This allows for dependency injection into the SyncWorker in a different
 module than the app module without having to create a custom WorkManager
 configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder<DelegatingWorker>()
    // Run sync as expedited work if the app is able to.
    // If not, it runs as regular work.
   .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
   .setConstraints(SyncConstraints)
    // Delegate to the SyncWorker.
   .setInputData(SyncWorker::class.delegatedData())
   .build()

val SyncConstraints
   get() = Constraints.Builder()
       .setRequiredNetworkType(NetworkType.CONNECTED)
       .build()

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

כשהרשת תהיה זמינה, ה-Worker ירוקן את תור העבודה הייחודי צוין על ידי SyncWorkName על ידי הענקת גישה ל-Repository המתאים במקרים שונים. אם הסנכרון נכשל, ה-method doWork() חוזרת עם Result.retry(). WorkManager ינסה לסנכרן שוב באופן אוטומטי עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff). אחרת, היא מחזירה Result.success() הושלמו סנכרון.

class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {

    override suspend fun doWork(): Result = withContext(ioDispatcher) {
        // First sync the repositories in parallel
        val syncedSuccessfully = awaitAll(
            async { topicRepository.sync() },
            async { authorsRepository.sync() },
            async { newsRepository.sync() },
        ).all { it }

        if (syncedSuccessfully) Result.success()
        else Result.retry()
    }
}

דוגמיות

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