آموزش Offline-First در اندروید: همگام سازی داده ها با سرور

برنامه نویسی اندروید
آموزش Offline-First در اندروید: همگام سازی داده ها با سرور

چرا پیاده سازی معماری Offline-First در اپلیکیشن های اندروید حیاتی است؟

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

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

آمارهای صنعتی نشان می دهند که بیش از ۶۸ درصد از کاربران اپلیکیشن هایی را که در صورت قطع چندثانیه ای اینترنت دچار توقف کامل می شوند، در همان هفته اول حذف می کنند. معماری مبتنی بر آفلاین این نرخ ریزش را تا ۷۴ درصد کاهش می دهد.

تغییر نگرش: آفلاین یک وضعیت طبیعی است، نه یک خطای سیستمی
در مهندسی مدرن اندروید، عدم دسترسی به اینترنت یک استثنا (Exception) تلقی نمی شود؛ بلکه یک حالت کاربری کاملا طبیعی است که اپلیکیشن باید با حفظ پایداری کامل و بدون وقفه در رابط کاربری به آن پاسخ دهد.

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

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

در این راهنما، معماری کامل آفلاین را با استفاده از کتابخانه های رسمی جت پک نظیر روم، WorkManager و فلوهای کاتلین پیاده سازی خواهیم کرد.

معماری سه لایه تک منبع حقیقت (Single Source of Truth)

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

معماری تک منبع حقیقت و لایه های آفلاین در اندروید
شکل ۱: دیاگرام معماری سه لایه تک منبع حقیقت از لایه رابط کاربری تا دیتابیس روم و موتور همگام ساز WorkManager

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

این ساختار جریان داده یک طرفه (Unidirectional Data Flow) مانع از ایجاد ناهماهنگی بین داده های نمایش داده شده روی صفحه و دیتای ذخیره شده در حافظه دستگاه می شود.

حتی اگر ارتباط اینترنتی در حین انتقال داده ها قطع گردد، دیتابیس محلی همواره آخرین حالت معتبر و پایدار اطلاعات را بدون هیچ تاخیری در اختیار لایه ارائه قرار می دهد.

توسعه دهندگان با بهره گیری از فریم ورک مدرن جت پک کامپوز می توانند این جریان های واکنشی را مستقیما به کامپوننت های بصری متصل کنند تا تغییرات به صورت بلادرنگ رندر شوند.

پیشنهاد مطالعه
آموزش دیتابیس Room در اندروید: ذخیره سازی محلی داده ها
آموزش دیتابیس Room در اندروید؛ راهنمای جامع ذخیره سازی محلی، تعریف Entity، کوئری های DAO با کاتلین Flow، مایگریشن خودکار و الگو های کشینگ آفلاین.
مشاهده مقاله ←

مانیتورینگ هوشمند وضعیت شبکه با ConnectivityManager و StateFlow

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

در سیستم عامل اندروید با ثبت یک NetworkCallback اختصاصی روی سرویس ConnectivityManager، تغییرات وضعیت اتصال به شبکه را مانیتور کرده و آن را در قالب یک StateFlow به کل سیستم گزارش می دهیم.

// رصد بلادرنگ وضعیت دسترسی به اینترنت با فلو
class NetworkObserver(private val context: Context) {
    private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val isOnline: Flow = callbackFlow {
        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) { trySend(true) }
            override fun onLost(network: Network) { trySend(false) }
        }
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        cm.registerNetworkCallback(request, callback)
        awaitClose { cm.unregisterNetworkCallback(callback) }
    }.distinctUntilChanged()
}

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

این مانیتورینگ کم مصرف تضمین می کند که هیچ درخواست بیهوده ای در زمان قطعی کامل اینترنت به ماژول رادیویی دستگاه ارسال نگردد تا شارژ باتری حفظ شود.

مدل سازی داده های محلی در Room با فیلدهای وضعیت همگام سازی

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

ما سه فیلد کلیدی به موجودیت های دیتابیس اضافه می کنیم: شناسه یکتای سمت کلاینت (clientUuid)، برچسب وضعیت همگام سازی (SyncStatus)، و زمان آخرین تغییر (updatedAt).

enum class SyncStatus {
    SYNCED, PENDING_INSERT, PENDING_UPDATE, PENDING_DELETE
}

@Entity(tableName = "notes")
data class NoteEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val title: String,
    val content: String,
    val updatedAt: Long = System.currentTimeMillis(),
    val syncStatus: SyncStatus = SyncStatus.PENDING_INSERT
)

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

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

این ساختار امکان حذف نرم (Soft Delete) را نیز فراهم می سازد؛ به این صورت که رکوردهای حذفی ابتدا با وضعیت PENDING_DELETE علامت گذاری شده و پس از تایید سرور، به صورت کامل از حافظه دیسک پاکسازی می شوند.

پیشنهاد مطالعه
آموزش رتروفیت Retrofit در اندروید: ارتباط با سرور و وب سرویس
آموزش رتروفیت Retrofit در اندروید با کاتلین؛ پیکربندی کلاینت OkHttp، مدیریت اینترسپتورها، احراز هویت توکن، سریالایزیشن JSON و خطایابی شبکه.
مشاهده مقاله ←

پیاده سازی به روزرسانی های خوش بینانه (Optimistic UI Updates)

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

پایپ لاین پردازش تراکنش های محلی و همگام سازی داده ها
شکل ۲: پایپ لاین پردازش تراکنش های محلی، صف بندی شبکه، ارسال درخواست امن و حل تعارض داده ها

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

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

در صورتی که سرور خطای غیرقابل جبرانی (مانند خطای اعتبارسنجی بیزینس) بازگرداند، سیستم با مکانیزم بازگشت به عقب (Rollback)، حالت قبلی داده ها را بازگردانده و پیام مناسبی به کاربر نشان می دهد.

به روزرسانی خوش بینانه حس سرعتی معادل با اپلیکیشن های نیتیو دسکتاپ به کاربر منتقل می کند و کندی های ذاتی شبکه های موبایل را کاملا پنهان می سازد.

مدیریت و زمان بندی همگام سازی پس زمینه با WorkManager

کتابخانه Android WorkManager استاندارد طلایی گوگل برای اجرای وظایف پس زمینه ای است که حتما باید حتی پس از بسته شدن برنامه یا ری استارت شدن گوشی تکمیل شوند.

مدیریت زمان بندی همگام سازی پس زمینه با WorkManager
شکل ۳: هاب زمان بندی WorkManager با شروط مصرف باتری، اتصال شبکه و سیاست بازگشت نمایی

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

برای همگام سازی داده ها از دو مدل ورکر استفاده می شود: همگام سازی یک باره فوری (One-Time) به محض اتصال مجدد اینترنت، و همگام سازی دوره ای (Periodic) برای واکشی جدیدترین تغییرات سرور.

// زمان بندی همگام سازی در WorkManager با شرایط شبکه
fun schedulePeriodicSync(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build()

    val syncRequest = PeriodicWorkRequestBuilder(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
        .build()

    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "app_data_sync_work",
        ExistingPeriodicWorkPolicy.KEEP,
        syncRequest
    )
}

قید NetworkType.CONNECTED تضمین می کند که وظیفه پردازشی تا زمان برقراری اینترنت معتبر در صف معلق بماند و از بیدار شدن های بی دلیل پردازنده جلوگیری شود.

همچنین سیاست بازگشت نمایی (Exponential Backoff) در صورت بروز خطاهای موقت سرور، فاصله زمانی تلاش های مجدد را به صورت تصاعدی افزایش می دهد تا سرور دچار ترافیک انفجاری نشود.

استراتژی های حل تعارض داده ها در سیستم های توزیع شده موبایل

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

استراتژی های پیشرفته حل تعارض داده ها در سیستم های موبایل
شکل ۴: تحلیل و مقایسه استراتژی های غلبه بر تعارض داده از آخرین تغییر برنده تا ادغام فیلدی

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

هشدار حیاتی: خطرات استفاده از تایم استمپ های محلی گوشی
هرگز ساعت سخت افزاری موبایل کاربر را مبنای قطعی حل تعارض قرار ندهید؛ چرا که کاربر می تواند ساعت گوشی را به صورت دستی تغییر دهد یا اختلاف منطقه زمانی باعث بازنویسی اشتباه داده ها شود. همواره از زمان سرور یا شماره های نسخه ترتیبی (Sequence Numbers) استفاده کنید.
استراتژی حل تعارض مکانیزم عملکرد مزیت اصلی ریسک یا محدودیت
آخرین تغییر برنده (LWW) مقایسه تایم استمپ میلی ثانیه ای کلاینت و سرور پیاده سازی فوق العاده ساده و بدون سربار امکان بازنویسی تصادفی اطلاعات به علت خطای ساعت
مرجعیت کامل سرور نسخه سمت سرور همواره بازنویسی کننده کلاینت است تضمین یکپارچگی قوانین تجاری و مالی نادیده گرفته شدن احتمالی تغییرات آفلاین کاربر
ادغام هوشمند فیلدها ترکیب فیلدهای ویرایش نشده از هر دو نسخه حداکثر نگه داری داده های هر دو طرف پیچیدگی بالا و نیاز به لاگ تغییرات ریزدانه
داوری مستقیم کاربر نمایش فرم مقایسه دو نسخه و انتخاب توسط کاربر عدم از دست رفتن هیچ داده ای بدون اطلاع ایجاد زحمت و کاهش تمرکز کاربر در برنامه

در بیشتر اپلیکیشن های سازمانی و نوت برداری، ترکیب رویکرد آخرین تغییر برنده با تایم استمپ های سرور (Server-Assigned Timestamps) تعادل ایده آلی میان سادگی و دقت برقرار می نماید.

طراحی API های مقاوم در برابر ارسال مجدد (Idempotent Design)

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

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

برای رفع این چالش، شناسه رکوردهای جدید همواره در سمت کلاینت با فرمت UUID v4 تولید شده و به همراه درخواست فرستاده می شود. سرور با ثبت این کلید در هدر X-Idempotency-Key از ثبت تکراری تراکنش ها جلوگیری می کند.

// تعریف اینترفیس سرویس رتروفیت با کلید عدم تکرار
interface SyncApiService {
    @POST("api/v1/sync/notes")
    suspend fun syncNote(
        @Header("X-Idempotency-Key") idempotencyKey: String,
        @Body noteDto: NoteDto
    ): Response
}

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

پیاده سازی الگوی مخزن داده آفلاین (Offline Repository Pattern)

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

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

// پیاده سازی مخزن داده آفلاین با هماهنگی دیتابیس و شبکه
class NoteRepositoryImpl(
    private val noteDao: NoteDao,
    private val apiService: SyncApiService
) : NoteRepository {

    override fun observeNotes(): Flow> =
        noteDao.getAllNotesFlow().map { it.map(NoteEntity::toDomain) }

    override suspend fun saveNote(title: String, content: String) {
        val entity = NoteEntity(title = title, content = content)
        noteDao.insertNote(entity)
    }

    override suspend fun syncPending() {
        noteDao.getPendingNotes().forEach { note ->
            val res = apiService.syncNote(note.id, note.toDto())
            if (res.isSuccessful) {
                noteDao.updateStatus(note.id, SyncStatus.SYNCED)
            }
        }
    }
}

این ساختار ایزوله مانع از وابستگی لایه های بالایی برنامه به جزییات شبکه شده و آزمون پذیری کد را به حداکثر می رساند.

ردیابی دقیق تغییرات با الگوی ثبت وقایع ویرایش (Mutation Journaling)

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

الگوی ثبت وقایع ویرایش (Mutation Journaling) تمامی تغییرات کاربر را به عنوان یک واقعه تغییرناپذیر در یک جدول اختصاصی به نام mutation_log ثبت می کند.

هر سطر در این جدول شامل نوع عملیات (INSERT، UPDATE، DELETE)، نام موجودیت، بدنه داده به صورت JSON، شناسه ترتیبی و زمان رخداد است.

در هنگام اتصال به شبکه، ورکر همگام ساز به جای ارسال موجودیت های پراکنده، وقایع این جدول را دقیقا به همان ترتیبی که کاربر انجام داده بازپخش (Replay) می کند و پس از تایید سرور، لاگ های پردازش شده را حذف می نماید.

این تکنیک بی نظیر مانع از تداخل های ترتیبی شده و قابلیت بازتولید وضعیت کلاینت را در صورت بروز خطاهای پیچیده فراهم می آورد.

همگرایی ریاضی داده ها با ساختارهای بدون تضاد (CRDTs)

یکی از پیشرفته ترین رویکردها در سیستم های توزیع شده موبایل و برنامه های همکاری جمعی نظیر اسناد مشترک یا تسک های تیمی، بهره گیری از ساختارهای داده تکرارپذیر بدون تعارض (Conflict-free Replicated Data Types یا CRDT) است.

در مدل سنتی، سرور باید داوری کند کدام تغییر ارجحیت دارد؛ اما در ساختارهای CRDT، عملیات به صورت ذاتی جابجایی پذیر و شرکت پذیر (Commutative & Associative) طراحی می شوند.

این خاصیت ریاضی تضمین می کند که فارغ از اینکه کلاینت ها با چه تاخیر زمانی یا به چه ترتیبی به شبکه متصل شوند، به محض رسیدن تمامی بسته های تغییرات به یک وضعیت کاملا یکسان و همگرا دست پیدا می کنند.

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

یکپارچه سازی با Paging ۳ و الگوی RemoteMediator برای داده های حجیم

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

کتابخانه Paging 3 گوگل کلاس قدرتمندی به نام RemoteMediator ارائه می دهد که پل ارتباطی رسمی میان دیتابیس روم و کلاینت شبکه رتروفیت به شمار می رود.

نکته مهندسی: ادغام با Jetpack Paging ۳ در سناریوهای آفلاین
هنگامی که دیتابیس محلی دارای هزاران رکورد است، ترکیب الگوی Offline-First با RemoteMediator در کتابخانه Paging ۳ تضمین می کند که بارگذاری صفحات داده از دیسک بدون مصرف بی رویه حافظه رم و با اسکرول نامحدود انجام گیرد.

در این الگو، لایه PagingSource مستقیما از روم داده ها را می خواند. به محض اینکه کاربر به انتهای لیست در حال نمایش نزدیک می شود، RemoteMediator فعال شده و صفحه بعدی را از سرور دانلود می کند و مستقیما درون دیتابیس محلی ذخیره می نماید.

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

تزریق وابستگی ماژول های همگام سازی با فریم ورک Hilt

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

ماژول اختصاصی SyncModule با فریم ورک Hilt نمونه های تکین این اجزا را در سطح چرخه حیات اپلیکیشن فراهم می سازد و امکان جایگزینی ساده آنها را در تست های واحد مهیا می کند.

// ماژول تزریق وابستگی همگام ساز در Hilt
@Module
@InstallIn(SingletonComponent::class)
object SyncDataModule {
    @Provides
    @Singleton
    fun provideNetworkObserver(@ApplicationContext ctx: Context) = NetworkObserver(ctx)

    @Provides
    @Singleton
    fun provideNoteRepository(
        dao: NoteDao,
        api: SyncApiService
    ): NoteRepository = NoteRepositoryImpl(dao, api)
}

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

مدیریت حجم کش محلی و سیاست های پاکسازی خودکار دیتابیس

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

در معماری های مدرن موبایل، استراتژی های پاکسازی خودکار کش (Cache Eviction) بر اساس الگوی کمترین استفاده اخیر (LRU) یا زمان سپری شده پیاده سازی می گردند.

به عنوان مثال، داده های همگام شده ای که بیش از ۳۰ روز از آخرین مشاهده آن ها گذشته است و وضعیت آن ها SYNCED است، به صورت دوره ای توسط یک ورکر پس زمینه پاکسازی می شوند تا فضا آزاد گردد.

نکته حیاتی این است که رکوردهای با وضعیت معلق (Pending) هرگز نباید تحت هیچ شرایطی پاک شوند تا تغییرات آفلاین کاربر از دست نرود.

آزمون های خودکار و شبیه سازی خطاهای شبکه در معماری آفلاین

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

با بهره گیری از ابزار هایی نظیر MockWebServer و پایگاه های داده درون حافظه ای Room (In-Memory Database)، می توان رفتارهای ریپازیتوری را در هنگام قطعی های متوالی ارزیابی کرد.

// تست اعتبارسنجی ثبت آفلاین در دیتابیس روم
@Test
fun saveNote_inOfflineMode_storesEntityWithPendingStatus() = runTest {
    val repository = NoteRepositoryImpl(fakeDao, mockApi)
    repository.saveNote("تست عنوان", "تست متن آفلاین")

    val notes = repository.observeNotes().first()
    assertEquals(1, notes.size)
    assertEquals(SyncStatus.PENDING_INSERT, fakeDao.getLastStatus())
}

علاوه بر آزمون های واحد، پیاده سازی آزمون های شبیه سازی مرگ ناگهانی فرآیند (Process Death) با ارسال سیگنال های قطعی سیستم عامل، اطمینان می دهد که تراکنش های نیمه کاره دیتابیس در تراکنش های امن (Transactions) لغو شده و دیتابیس هرگز دچار خرابی ساختار نمی شود.

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

بهینه سازی پهنای باند و همگام سازی دلتا (Delta Syncing)

در شبکه های موبایل با اینترنت محدود و بسته های داده گران قیمت، ارسال کل محتوای دیتابیس در هر بار همگام سازی اتلاف شدید منابع است. الگوی همگام سازی دلتا (Delta Sync) راهکاری استاندارد برای انتقال صرفا بخش های تغییریافته است.

در این الگو، به جای ارسال کل موجودیت ها، کلاینت در زمان استعلام از سرور آخرین زمان همگام سازی موفق خود (lastSyncTimestamp) را ارسال می کند. سرور صرفا رکوردهایی را که پس از آن تاریخ ایجاد یا ویرایش شده اند بازمی گرداند.

علاوه بر این، استفاده از هدرهای استاندارد ETag و شروط شرطی HTTP نظیر If-None-Match مانع از دانلود مجدد اطلاعات بدون تغییر می شود؛ چنانچه دیتای سرور دست نخورده باشد، کد وضعیت 304 Not Modified بدون هیچ بدنه اضافی برگشت داده می شود.

همچنین با پیکربندی قید NetworkCapabilities.NET_CAPABILITY_NOT_METERED در ورکرها، همگام سازی فایل های حجیم (نظیر تصاویر پیوست یا ویدیوها) صرفا به زمان اتصال به شبکه های وای فای رایگان موکول می شود تا حجم اینترنت سیم کارت کاربر حفظ گردد.

ترکیب فشرده سازی GZIP روی هدرهای رتروفیت و بهره گیری از فرمت های بهینه نظیر Protocol Buffers در مقایسه با JSON سنتی، مصرف ترافیک شبکه را تا ۵۸ درصد کاهش می دهد.

چک لیست ممیزی معماری Offline-First قبل از عرضه در پروداکشن

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

  • عدم مسدود شدن ترد اصلی: تمامی عملیات خواندن و نوشتن دیتابیس در تردهای پس زمینه و دیسپچر Dispatchers.IO اجرا شوند.
  • تولید شناسه سمت کلاینت: همه رکوردهای جدید از کلیدهای تصادفی UUID برای جلوگیری از تداخل استفاده کنند.
  • مدیریت بازگشت به عقب: در صورت رد شدن تراکنش توسط سرور، مکانیزم رول بک به وضعیت قبلی فعال باشد.
  • شروط بهینه در WorkManager: قیود مصرف باتری و اتصال پایدار شبکه برای جاب های سنگین سینک تنظیم شده باشند.
  • پرهیز از پاکسازی رکوردهای معلق: الگوریتم های پاکسازی کش هرگز آیتم های همگام نشده را حذف نکنند.
  • پوشش تست واحد: آزمون های شبیه سازی قطعی ناگهانی شبکه در فرآیند بیلد خودکار پاس شوند.
  • نمایش وضعیت شبکه به کاربر: بنر غیرمزاحم در بالای صفحه برای اطلاع رسانی کار در حالت آفلاین تعبیه گردد.

پرسش های متداول درباره معماری Offline-First و همگام سازی داده ها (FAQ)

۱. تفاوت معماری Offline-First با کش سنتی در اندروید چیست؟

در کش سنتی، شبکه منبع اصلی داده است و دیتابیس صرفا نسخه ای برای بارگذاری سریع تر نگه می دارد، در حالی که در معماری Offline-First دیتابیس محلی تنها منبع حقیقت است و تمام نوشتن ها و خواندن ها ابتدا در حافظه محلی اعمال می شوند.

۲. چگونه از ایجاد رکوردهای تکراری در زمان همگام سازی جلوگیری کنیم؟

با تولید شناسه های تصادفی UUID در سمت کلاینت و ارسال آن ها در قالب کلید عدم تکرار (Idempotency Key) به سرور، سرور می تواند درخواست های مکرر یکسان را شناسایی کرده و از ثبت تراکنش تکراری ممانعت نماید.

۳. چرا نباید داده های شبکه مستقیما به لایه UI ارسال شوند؟

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

۴. نقش کتابخانه WorkManager در همگام سازی آفلاین چیست؟

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

۵. چگونه حذف رکوردها در حالت آفلاین مدیریت می شود؟

با استفاده از تکنیک حذف نرم (Soft Delete)، رکوردها فورا از دیسک پاک نمی شوند بلکه با وضعیت معلق برای حذف علامت گذاری می گردند تا پس از تایید حذف توسط سرور، به شکل قطعی از دیتابیس محلی پاکسازی شوند.

۶. در صورت تضاد ویرایش همزمان آفلاین و آنلاین کدام نسخه اولویت دارد؟

بسته به نیازمندی بیزینس از استراتژی آخرین تغییر برنده (LWW) با مقایسه تایم استمپ، یا مرجعیت کامل سرور استفاده می شود؛ برای اسناد حساس تر می توان از الگوریتم های ادغام فیلدی یا پرسش مستقیم از کاربر بهره برد.

۷. آیا استفاده از Room برای پروژه های با حجم دیتای بسیار زیاد مناسب است؟

بله، پایگاه داده Room بر پایه موتور بهینه SQLite ساخته شده است و با استفاده از ایندکس گذاری مناسب روی کلیدها و اعمال سیاست های پاکسازی خودکار کش، عملکردی با سرعت فوق العاده و تاخیر در حد میلی ثانیه ارائه می دهد.

۸. چگونه تغییرات وضعیت شبکه بدون مصرف زیاد باتری مانیتور می شوند؟

به جای بررسی مداوم (Polling)، با ثبت NetworkCallback در سیستم عامل، اندروید صرفا در لحظه تغییر واقعی وضعیت کارت شبکه رویدادی را مخابره می کند که کمترین مصرف انرژی ممکن را برای پردازنده به همراه دارد.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.