ساختن برنامه با اولویت آفلاین

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

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

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

  • پهنای باند اینترنت محدود است
  • قطعی‌های موقت اتصال، مثلاً وقتی در آسانسور یا تونل هستید
  • دسترسی گاه‌به‌گاه به داده‌ها—برای مثال، رایانه‌های لوحی‌ای که فقط Wi-Fi دارند

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

  • بدون اتصال شبکه مطمئن، قابل‌استفاده باقی بماند
  • داده‌های محلی را فوراً به کاربران ارائه دهید و منتظر تکمیل یا ناموفق شدن اولین تماس شبکه نمانید
  • داده‌ها را به‌گونه‌ای واکشی کنید که وضعیت باتری و داده‌ها را درنظر بگیرد—برای مثال، با درخواست واکشی داده‌ها فقط در شرایط بهینه، مانند زمانی که دستگاه درحال شارژ است یا به Wi-Fi متصل است

برنامه‌ای که این معیارها را برآورده کند اغلب برنامه «آفلاین-اولویت» نامیده می‌شود.

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

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

  • خواندن: بازیابی داده‌ها برای استفاده در بخش‌های دیگر برنامه، مثلاً نمایش اطلاعات به کاربر. در Compose، معمولاً این کار را با مشاهده state انجام می‌دهید. وقتی واسط کاربری شما منبع داده محلی را به‌عنوان وضعیت مشاهده می‌کند، صفحه به‌طور خودکار جدیدترین داده‌های محلی را منعکس می‌کند.
  • نوشتن: حفظ ورودی کاربر برای بازیابی در آینده. در Compose، معمولاً این کار را بااستفاده از رویدادها و کنش‌های ارسال‌شده از واسط کاربر به ViewModel انجام می‌دهید.

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

مدل‌سازی داده‌ها در برنامه آفلاین-محور

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

  • منبع داده محلی
  • منبع داده شبکه
لایه داده «ابتدا آفلاین» از هر دو منبع داده محلی و شبکه تشکیل شده است
شکل ۱: مخزن آفلاین-نخست.

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

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

  • منابع داده‌های ساختاریافته، مانند پایگاه‌های داده رابطه‌ای مثل Room
  • منابع داده‌های ساختارنیافته—برای مثال، میان‌گیری پروتکل با DataStore
  • فایل‌های ساده

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

منبع داده شبکه وضعیت واقعی برنامه است. در بهترین حالت، منبع داده محلی با منبع داده شبکه همگام‌سازی می‌شود. منبع داده محلی همچنین می‌تواند از منبع داده شبکه عقب بماند، که در این صورت برنامه باید هنگام بازگشت به حالت آنلاین به‌روز شود. برعکس، منبع داده شبکه ممکن است از منبع داده محلی عقب بماند تا زمانی که برنامه بتواند آن را هنگام بازگشت اتصال به‌روز کند. لایه دامنه و لایه میانای کاربر برنامه هرگز نباید مستقیماً با لایه شبکه ارتباط برقرار کنند. مسئولیت repository میزبان است که با آن ارتباط برقرار کند و از آن برای به‌روزرسانی منبع داده محلی استفاده کند.

همگام‌سازی سفارشی درمقابل همگام‌سازی مدیریت‌شده

ساختن لایه داده آفلاین-محور را می‌توان به دو روش اصلی انجام داد:

  • پشته همگام‌سازی سفارشی: پایگاه‌های داده محلی را با Room یا فایل‌ها در فضای ذخیره‌سازی محلی نگهداری می‌کنید، کارخواه‌های شبکه سفارشی می‌سازید، و کارگران تطبیق را با WorkManager زمان‌بندی می‌کنید. این رویکرد حداکثر کنترل را بر طرح‌واره‌ها و الگوریتم‌های سفارشی حل تعارض فراهم می‌کند.
  • خدمات ابری مدیریت‌شده: راه‌حل‌هایی مانند Cloud Firestore می‌توانند یک حافظه نهان محلی همه‌کاره و جاری‌سازی ابری هم‌زمان ارائه دهند. کیت توسعه نرم‌افزار به‌طور خودکار حافظه نهان دیسک، صف‌بندی نوشتن آفلاین، و همگام‌سازی دوطرفه را مدیریت می‌کند و جاری‌سازی‌های داده‌های هم‌زمان را مستقیماً به مخزن شما ارائه می‌دهد. برای بخش‌های پشتیبان PostgreSQL رابطه‌ای، Firebase SQL Connect کیت‌های توسعه نرم‌افزار مشتری Kotlin با ایمنی تایپی تولید می‌کند که به‌طور یکپارچه با مخازن شما ادغام می‌شوند. برای داده‌های ساختارنیافته (مانند رسانه، فایل‌های بزرگ، یا پشتیبان‌گیری‌ها)، فضای ذخیره‌سازی ابری ویژه Firebase فضای ذخیره‌سازی شیء میزبانی‌شده در فضای ابری را با عملیات بارگذاری و بارگیری ارائه می‌دهد که به‌طور خودکار درصورت قطع شدن شبکه متوقف و ازسر گرفته می‌شود.

درحال افشای منابع

منابع داده محلی و شبکه می‌توانند ازنظر نحوه خواندن و نوشتن برنامه شما در آن‌ها تفاوت‌های اساسی داشته باشند. پُرسمان منبع داده محلی می‌تواند سریع و انعطاف‌پذیر باشد، مثلاً هنگام استفاده از پُرسمان‌های 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,
)

خوانده‌شده

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

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

class TopicsViewModel(
    offlineFirstTopicsRepository: OfflineFirstTopicsRepository
) : ViewModel() {

    val topics: StateFlow<List<Topic>> = offlineFirstTopicsRepository.getTopicsStream()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )
}

در برنامه Jetpack Compose، از ViewModel برای پل زدن بین لایه داده و واسط کاربر استفاده کنید. در ViewModel، بااستفاده از عملگر stateIn،‏ Flow را به StateFlow تبدیل کنید. سپس Composableها آن حالت‌ها را بااستفاده از collectAsStateWithLifecycle() جمع‌آوری می‌کنند و به‌طور خودکار اشتراک‌ها را به‌شیوه‌ای آگاه از چرخه حیات مدیریت می‌کنند.

برای اطلاعات بیشتر درباره collectAsStateWithLifecycle()، حالت و Jetpack Compose را ببینید.

استراتژی‌های مدیریت خطا

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

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

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

می‌توانید از عملگر 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()) }
}

برای رویکردی انعطاف‌پذیرتر، راه‌حل LCE (خطای بار کردن محتوا) را درنظر بگیرید. در LCE، وقتی هنگام خواندن خطایی رخ می‌دهد، حالت خطا را نشان می‌دهید. معمولاً، با مدل‌سازی حالت‌های میانای کاربر به‌عنوان کلاس‌های مهروموم‌شده Kotlin، به LCE دست پیدا می‌کنید.

// Define the LCE UI state
sealed interface AuthorUiState {
    data object Loading : AuthorUiState
    data class Success(val author: Author) : AuthorUiState
    data object Error : AuthorUiState
}

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

    // Observe author information and map to LCE state
    val authorUiState: StateFlow<AuthorUiState> =
        authorsRepository.getAuthorStream(id = authorId)
            .map<Author, AuthorUiState> { author ->
                AuthorUiState.Success(author)
            }
            .catch { emit(AuthorUiState.Error) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = AuthorUiState.Loading
            )
}

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

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

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

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

خواندن داده‌ها با عقب‌گرد نمایی
شکل ۲: خواندن داده‌ها با عقب‌گرد نمایی.

معیارهای ارزیابی اینکه آیا برنامه همچنان عقب‌نشینی می‌کند یا نه شامل موارد زیر است:

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

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

خواندن داده‌ها با پایشگرها و صف‌های شبکه
شکل ۳: صف‌های خواندن با پایش شبکه.

نوشتن

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

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

در قطعه کد بالا، میانای برنامه‌سازی کاربردی ناهمگام انتخابی Coroutines است زیرا روش تعلیق می‌شود.

نوشتن استراتژی‌ها

هنگام نوشتن داده‌ها در برنامه‌های آفلاین-اولویت، سه استراتژی برای درنظر گرفتن وجود دارد. انتخاب شما به نوع داده‌ای که نوشته می‌شود و الزامات برنامه بستگی دارد:

نوشتن فقط آنلاین

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

فقط نوشتن آنلاین
شکل ۴: نوشتن‌های فقط آنلاین.

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

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

نوشتن‌های در صف

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

نوشتن صف‌ها با تکرار مجدد
شکل ۵: صف‌های نوشتن با تکرار مجدد.

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

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

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

نوشتن تنبل

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

نوشتن تنبل با پایش شبکه
شکل ۶: نوشتن تنبل.

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

همگام‌سازی و حل تداخل

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

  • همگام‌سازی مبتنی بر کشیدن
  • همگام‌سازی مبتنی بر ارسال

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

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

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

همگام‌سازی مبتنی بر کشیدن
شکل ۷: همگام‌سازی مبتنی بر کشیدن: دستگاه 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
}

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

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

همگام‌سازی مبتنی بر ارسال

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

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

پس‌از دریافت اعلان کهنه، برنامه به شبکه دسترسی پیدا می‌کند تا فقط داده‌هایی را که به‌عنوان کهنه علامت‌گذاری شده‌اند به‌روز کند. این کار به Repository واگذار می‌شود که به منبع داده شبکه دسترسی پیدا می‌کند و داده‌های واکشی‌شده را در منبع داده محلی حفظ می‌کند. Since the repository exposes its data with observable types, readers are notified of any changes.

class UserDataRepository(...) {

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

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

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

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

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

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

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

حل تعارض

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

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

آخرین نوشتن برنده می‌شود

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

حل‌وفصل تعارض «آخرین نوشتن برنده است»
شکل ۹: «آخرین نوشتن برنده می‌شود» منبع حقیقت برای داده‌ها توسط آخرین نهادی که داده‌ها را می‌نویسد تعیین می‌شود.

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

‫WorkManager در برنامه‌های آفلاین-اولویت

در هر دو استراتژی خواندن و نوشتن که قبلاً پوشش داده شد، دو ابزار رایج وجود دارد:

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

هر دو مورد نمونه‌هایی از کار مداوم هستند که WorkManager در آن‌ها برتری دارد.

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

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

این کنش‌ها در نمودار زیر نشان داده شده است:

همگام‌سازی داده‌ها در برنامه «اکنون در Android»
شکل ۱۰: همگام‌سازی داده‌ها در برنامه 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 زیر برنامه‌های آفلاین-محور را نشان می‌دهند. برای دیدن این راهنمایی در عمل، آن‌ها را کاوش کنید: