یک برنامه آفلاین بسازید

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

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

در دسترس بودن شبکه همیشه تضمین شده نیست. دستگاه‌ها معمولاً دوره‌هایی از اتصال شبکه ضعیف یا کند دارند. کاربران ممکن است موارد زیر را تجربه کنند:

  • پهنای باند اینترنت محدود
  • وقفه های اتصال موقت، مانند زمانی که در آسانسور یا تونل هستید.
  • دسترسی گاه به گاه به داده ها به عنوان مثال، تبلت های فقط وای فای.

صرف نظر از دلیل، اغلب ممکن است یک برنامه در این شرایط به اندازه کافی کار کند. برای اطمینان از عملکرد صحیح برنامه شما در حالت آفلاین، باید بتواند کارهای زیر را انجام دهد:

  • بدون اتصال شبکه قابل اعتماد قابل استفاده باقی بماند.
  • به جای اینکه منتظر بمانید تا اولین تماس شبکه تکمیل یا شکست بخورد، بلافاصله داده های محلی را به کاربران ارائه دهید.
  • داده ها را به گونه ای واکشی کنید که از وضعیت باتری و داده آگاه باشد. به عنوان مثال، تنها با درخواست واکشی داده در شرایط بهینه، مانند هنگام شارژ یا WiFi.

برنامه ای که می تواند معیارهای بالا را برآورده کند، اغلب برنامه آفلاین اول نامیده می شود.

اولین برنامه آفلاین طراحی کنید

هنگام طراحی یک برنامه آفلاین اول باید از لایه داده و دو عملیات اصلی که می توانید روی داده های برنامه انجام دهید شروع کنید:

  • Reads : بازیابی داده ها برای استفاده توسط بخش های دیگر برنامه مانند نمایش اطلاعات به کاربر.
  • می نویسد : ورودی کاربر مداوم برای بازیابی بعدی.

مخازن در لایه داده مسئول ترکیب منابع داده برای ارائه داده های برنامه هستند. در یک برنامه آفلاین اول، حداقل باید یک منبع داده وجود داشته باشد که برای انجام حیاتی ترین وظایف خود نیازی به دسترسی به شبکه نداشته باشد. یکی از این وظایف حیاتی خواندن داده ها است.

مدل‌سازی داده‌ها در یک برنامه آفلاین

یک برنامه آفلاین اول حداقل 2 منبع داده برای هر مخزنی که از منابع شبکه استفاده می کند دارد:

  • منبع داده های محلی
  • منبع داده شبکه
یک لایه داده آفلاین اول از هر دو منبع داده محلی و شبکه تشکیل شده است
شکل 1 : مخزن اول آفلاین

منبع داده های محلی

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

  • منابع داده های ساختاریافته، مانند پایگاه داده های رابطه ای مانند اتاق .
  • منابع داده بدون ساختار به عنوان مثال، بافرهای پروتکل با 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,
)

می خواند

Reads عملیات اساسی روی داده های برنامه در یک برنامه آفلاین است. بنابراین باید مطمئن شوید که برنامه شما می‌تواند داده‌ها را بخواند و به محض اینکه داده‌های جدید در دسترس هستند، برنامه می‌تواند آن‌ها را نمایش دهد. برنامه ای که می تواند این کار را انجام دهد یک برنامه واکنشی است زیرا 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()) }
}

منبع داده شبکه

اگر هنگام خواندن داده ها از منبع داده شبکه، خطاهایی رخ دهد، برنامه باید از یک اکتشافی برای واکشی مجدد داده استفاده کند. اکتشافی های رایج عبارتند از:

عقب نشینی نمایی

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

خواندن داده ها با عقب نشینی نمایی
شکل 2 : خواندن داده ها با عقب نشینی نمایی

معیارهای ارزیابی اینکه آیا برنامه باید به عقب نشینی ادامه دهد یا خیر عبارتند از:

  • نوع خطایی که منبع داده شبکه نشان داد. برای مثال، باید تماس‌های شبکه‌ای را دوباره امتحان کنید که خطایی نشان دهنده عدم اتصال است. برعکس، نباید درخواست‌های 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 : فقط آنلاین می نویسد

این استراتژی اغلب برای نوشتن تراکنش هایی استفاده می شود که باید به صورت آنلاین در زمان واقعی انجام شوند. مثلا حواله بانکی. از آنجایی که نوشتن ممکن است با شکست مواجه شود، اغلب لازم است که به کاربر اطلاع داده شود که نوشتن ناموفق بوده یا از تلاش کاربر برای نوشتن داده در وهله اول جلوگیری شود. برخی از استراتژی هایی که می توانید در این سناریوها به کار ببرید ممکن است شامل موارد زیر باشد:

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

صف می نویسد

هنگامی که یک شی دارید که می خواهید بنویسید، آن را در یک صف قرار دهید. وقتی برنامه دوباره آنلاین شد، صف را با عقب نشینی نمایی تخلیه کنید. در Android، خالی کردن صف آفلاین کار مداومی است که اغلب به WorkManager واگذار می‌شود.

صف ها را با تلاش مجدد بنویسید
شکل 5 : صف ها را با تلاش مجدد بنویسید

این رویکرد انتخاب خوبی است اگر:

  • ضروری نیست که داده ها همیشه در شبکه نوشته شوند.
  • معامله به زمان حساس نیست.
  • در صورت شکست عملیات، اطلاع دادن به کاربر ضروری نیست.

موارد استفاده برای این رویکرد شامل رویدادهای تجزیه و تحلیل و ورود به سیستم است.

تنبل می نویسد

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

تنبل با مانیتورینگ شبکه می نویسد
شکل 6 : تنبل می نویسد

این رویکرد زمانی که داده ها برای برنامه حیاتی هستند، انتخاب صحیحی است. برای مثال، در یک برنامه لیست کارهای آفلاین، ضروری است که هر کاری که کاربر به صورت آفلاین اضافه می کند، به صورت محلی ذخیره شود تا از خطر از دست رفتن داده ها جلوگیری شود.

همگام سازی و حل تعارض

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

  • همگام سازی مبتنی بر کشش
  • همگام سازی مبتنی بر فشار

همگام سازی مبتنی بر کشش

در همگام سازی مبتنی بر کشش، برنامه به شبکه دسترسی پیدا می کند تا آخرین داده های برنامه را در صورت تقاضا بخواند. یک روش اکتشافی رایج برای این رویکرد مبتنی بر ناوبری است، که در آن برنامه فقط داده ها را درست قبل از ارائه به کاربر واکشی می کند.

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

همگام سازی بر اساس کشش
شکل 7 : همگام سازی مبتنی بر کشش: دستگاه A فقط برای صفحه های A و B به منابع دسترسی دارد، در حالی که دستگاه B فقط برای صفحه های B، C و D به منابع دسترسی دارد.

برنامه‌ای را در نظر بگیرید که در آن از نشانه‌های صفحه برای واکشی موارد در یک لیست پیمایش بی‌پایان برای یک صفحه خاص استفاده می‌شود. پیاده سازی ممکن است با تنبلی به شبکه برسد، داده ها را در منبع داده محلی حفظ کند و سپس از منبع داده محلی خوانده شود تا اطلاعات را به کاربر ارائه دهد. در مواردی که اتصال شبکه وجود ندارد، مخزن ممکن است داده ها را به تنهایی از منبع داده محلی درخواست کند. این الگویی است که توسط کتابخانه صفحه‌بندی Jetpack با RemoteMediator API استفاده می‌شود.

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
}

مزایا و معایب همگام سازی مبتنی بر کشش در جدول زیر خلاصه شده است:

مزایا معایب
اجرای نسبتا آسان. مستعد استفاده سنگین از داده ها این به این دلیل است که بازدیدهای مکرر از یک مقصد ناوبری باعث بازیابی غیر ضروری اطلاعات بدون تغییر می شود. شما می توانید از طریق کش مناسب این مشکل را کاهش دهید. این را می توان در لایه UI با اپراتور cachedIn یا در لایه شبکه با کش HTTP انجام داد.
داده هایی که مورد نیاز نیستند هرگز واکشی نمی شوند. با داده‌های رابطه‌ای به خوبی مقیاس نمی‌شود زیرا مدل کشیده‌شده باید خود کافی باشد. اگر مدلی که همگام‌سازی می‌شود به مدل‌های دیگری بستگی دارد که باید واکشی شوند تا خودش را پر کند، مشکل استفاده از داده‌های سنگین که قبلاً ذکر شد مهم‌تر می‌شود. علاوه بر این، ممکن است باعث وابستگی بین مخازن مدل والد و مخازن مدل تو در تو شود.

همگام سازی مبتنی بر فشار

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

همگام سازی مبتنی بر فشار
شکل 8 : همگام سازی مبتنی بر فشار: شبکه در صورت تغییر داده ها به برنامه اطلاع می دهد و برنامه با واکشی داده های تغییر یافته پاسخ می دهد.

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

class UserDataRepository(...) {

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

در این رویکرد، برنامه بسیار کمتر به منبع داده شبکه وابسته است و می تواند بدون آن برای مدت زمان طولانی کار کند. در حالت آفلاین هم دسترسی خواندن و نوشتن را ارائه می دهد زیرا فرض بر این است که آخرین اطلاعات را از منبع داده شبکه به صورت محلی دارد.

مزایا و معایب همگام سازی مبتنی بر فشار در جدول زیر خلاصه شده است:

مزایا معایب
برنامه می تواند به طور نامحدود آفلاین بماند. نسخه‌سازی داده‌ها برای حل تعارض بی‌اهمیت است.
حداقل استفاده از داده این برنامه فقط داده هایی را که تغییر کرده اند واکشی می کند. شما باید نگرانی های مربوط به نوشتن را در طول همگام سازی در نظر بگیرید.
برای داده های رابطه ای خوب کار می کند. هر مخزن فقط مسئول واکشی داده ها برای مدلی است که پشتیبانی می کند. منبع داده شبکه باید از همگام سازی پشتیبانی کند.

همگام سازی ترکیبی

برخی از برنامه ها از یک رویکرد ترکیبی استفاده می کنند که بسته به داده ها، کشش یا فشار است. به عنوان مثال، یک برنامه رسانه اجتماعی ممکن است از همگام‌سازی مبتنی بر کشش برای واکشی فید زیر در صورت تقاضای کاربر استفاده کند، زیرا به‌روزرسانی‌های فید فراوان است. همان برنامه ممکن است استفاده از همگام سازی مبتنی بر فشار را برای داده های مربوط به کاربر وارد شده از جمله نام کاربری، تصویر نمایه و غیره انتخاب کند.

در نهایت، انتخاب همگام‌سازی آفلاین به نیازهای محصول و زیرساخت‌های فنی موجود بستگی دارد.

حل تعارض

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

حل تعارض اغلب نیاز به نسخه سازی دارد. برنامه باید مقداری حسابداری انجام دهد تا تغییرات رخ داده را پیگیری کند. این امکان را به آن می دهد تا متادیتا را به منبع داده شبکه منتقل کند. منبع داده شبکه مسئولیت ارائه منبع مطلق حقیقت را دارد. بسته به نیاز برنامه، طیف گسترده ای از استراتژی ها برای حل تعارض در نظر گرفته می شود. برای برنامه های تلفن همراه یک رویکرد رایج "آخرین نوشتن برنده" است.

آخرین نوشته برنده است

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

آخرین نوشتن برنده حل تعارض است
شکل 9 : "آخرین نوشتن برنده می شود" منبع حقیقت برای داده ها توسط آخرین موجودی که داده را می نویسد تعیین می شود.

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

WorkManager در برنامه های آفلاین اول

در هر دو استراتژی خواندن و نوشتن که در بالا توضیح داده شد، دو ابزار مشترک وجود داشت:

  • صف ها
    • Reads: برای به تعویق انداختن خواندن تا زمانی که اتصال شبکه در دسترس باشد استفاده می شود.
    • Writes: برای به تعویق انداختن نوشتن تا زمانی که اتصال شبکه در دسترس باشد، و برای درخواست نوشتن برای تلاش مجدد استفاده می شود.
  • مانیتورهای اتصال شبکه
    • خواندن: به عنوان یک سیگنال برای تخلیه صف خواندن هنگام اتصال برنامه و برای همگام سازی استفاده می شود
    • Writes: به عنوان سیگنال برای تخلیه صف نوشتن هنگام اتصال برنامه و برای همگام سازی استفاده می شود

هر دو مورد نمونه هایی از کار مداوم هستند که WorkManager در آن برتری دارد. برای مثال در برنامه نمونه Now in Android ، WorkManager هم به عنوان صف خواندن و هم به عنوان مانیتور شبکه هنگام همگام سازی منبع داده محلی استفاده می شود. در هنگام راه اندازی، برنامه اقدامات زیر را انجام می دهد:

  1. کار همگام‌سازی خواندن را در صف قرار دهید تا مطمئن شوید که بین منبع داده محلی و منبع داده شبکه برابری وجود دارد.
  2. صف همگام‌سازی خواندن را خالی کنید و وقتی برنامه آنلاین است، همگام‌سازی را شروع کنید.
  3. خواندن را از منبع داده شبکه با استفاده از backoff نمایی انجام دهید.
  4. نتایج خواندن را در منبع داده محلی حفظ کنید تا هرگونه تضاد احتمالی را حل کند.
  5. داده‌های منبع داده محلی را برای مصرف سایر لایه‌های برنامه در معرض دید قرار دهید.

موارد فوق در نمودار زیر نشان داده شده است:

همگام سازی داده ها در برنامه Now in Android
شکل 10 : همگام سازی داده ها در برنامه Now in 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 مناسب تخلیه می کند. اگر همگام سازی ناموفق باشد، متد doWork() با Result.retry() باز می گردد. WorkManager به طور خودکار همگام سازی را با عقب نشینی نمایی دوباره امتحان می کند. در غیر این صورت، 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 زیر برنامه‌های آفلاین را نشان می‌دهند. برای دیدن این راهنمایی در عمل، آنها را کاوش کنید:

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}