RemoteMediator والتخزين المؤقت
اجمع بين Paging الشبكة وقاعدة البيانات
RemoteMediator والتخزين المؤقت درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 4 دروس في المجموع.
التصفح بالصفحات مع أولوية عدم الاتصال
يعمل PagingSource عادي يعتمد على الشبكة، ولكن إذا انقطع الاتصال تصبح القائمة فارغة. ولإنشاء تطبيق متين، نريد أن تكون قاعدة البيانات هي المصدر الوحيد للحقيقة، وأن تملأها الشبكة في الخلفية.
وهذا بالضبط ما يتيحه RemoteMediator: تحميل الصفحات من قاعدة البيانات المحلية، وجلب المزيد من الشبكة عند نفاد البيانات الموجودة في قاعدة البيانات.
البنية
مع RemoteMediator، تتدفق البيانات في اتجاه واحد:
- تعرض واجهة المستخدم الصفحات من
PagingSourceتابع لـ Room - عندما تقترب Room من نهاية البيانات، يجلب
RemoteMediatorالصفحة التالية من الشبكة - تُكتب نتيجة الشبكة في Room
- تُصدر Room الصفوف الجديدة، فتتحدّث واجهة المستخدم
يرى المستخدم دائمًا البيانات المخزّنة مؤقتًا فورًا، حتى عند عدم الاتصال.
// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directlyPagingSource من Room مجانًا
يمكن لـ Room إنشاء PagingSource تلقائيًا. ما عليك سوى تعريف استعلام يعيد PagingSource<Int, Entity>، وستتولى Room تنفيذه.
وهذا هو المصدر الذي تحمّل منه واجهة المستخدم الصفحات فعليًا.
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY position ASC")
fun pagingSource(): PagingSource<Int, ArticleEntity>
}التوريث من RemoteMediator
يحتوي RemoteMediator<Key, Value> على دالة مطلوبة واحدة هي load()، بالإضافة إلى دالة اختيارية هي initialize().
تتلقى load() قيمة LoadType (REFRESH أو PREPEND أو APPEND) وحالة PagingState الحالية، ثم تعيد قيمة MediatorResult.
import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
// override suspend fun load(loadType, state): MediatorResult
}معالجة LoadType
في load()، تقرر الصفحة التي يجب جلبها بناءً على LoadType:
- REFRESH - تحميل الصفحة الأولى (أو الصفحة القريبة من موضع الارتساء)
- PREPEND - لا يوجد عادةً ما يجب فعله؛ أعد
endOfPaginationReached = true - APPEND - تحميل الصفحة التي تلي آخر صفحة لدينا
val page: Int = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastKey = db.remoteKeyDao().last()?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)
lastKey
}
}جدول المفاتيح البعيدة
بخلاف PagingSource، لا يستطيع RemoteMediator إعادة مفاتيح الصفحة التالية أو السابقة مباشرةً، بل يجب حفظها. خزّنها في جدول صغير للمفاتيح البعيدة إلى جانب بياناتك.
يتيح لك ذلك استئناف عمليتي APPEND وPREPEND بعد إعادة تشغيل التطبيق.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "remote_keys")
data class RemoteKey(
@PrimaryKey val articleId: Int,
val prevKey: Int?,
val nextKey: Int?
)كتابة نتائج الشبكة في Room
بعد جلب صفحة، اكتب العناصر ومفاتيحها في Room داخل معاملة. عند تنفيذ REFRESH، امسح الجداول القديمة أولًا حتى تظل ذاكرة التخزين المؤقت متسقة.
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.remoteKeyDao().clearAll()
db.articleDao().clearAll()
}
val nextKey = if (response.items.isEmpty()) null else page + 1
db.remoteKeyDao().insertAll(
response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
)
db.articleDao().insertAll(response.items.map { it.toEntity() })
}إعادة MediatorResult
أنهِ load() بإعادة قيمة MediatorResult:
Success(endOfPaginationReached = true)عند عدم وجود صفحات أخرىSuccess(endOfPaginationReached = false)عند احتمال وجود صفحات إضافيةError(e)عند الفشل، وتظهر النتيجة كـLoadState.Error
return try {
val response = api.getArticles(page = page, size = state.config.pageSize)
db.withTransaction { /* write items + keys */ }
MediatorResult.Success(
endOfPaginationReached = response.items.isEmpty()
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}توصيل Pager بـ remoteMediator
أنشئ Pager باستخدام كل من remoteMediator وpagingSourceFactory التابع لـ Room. يملأ الوسيط Room، بينما تحمّل واجهة المستخدم الصفحات من Room.
لأنه يستخدم واجهة برمجة تطبيقات تجريبية، فعّلها باستخدام @OptIn(ExperimentalPagingApi::class).
@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = ArticleRemoteMediator(api, db),
pagingSourceFactory = { db.articleDao().pagingSource() }
).flowالبحث عن المفاتيح لعملية APPEND
لتحميل APPEND، تحتاج إلى nextKey للعنصر الأخير الذي تم تحميله. استخدم ارتساء PagingState للعثور على المفتاح البعيد الصحيح من جدول المفاتيح.
تجعل الدوال المساعدة load() سهلة القراءة عند التعامل مع REFRESH وAPPEND وPREPEND.
private suspend fun lastRemoteKey(
state: PagingState<Int, ArticleEntity>
): RemoteKey? {
return state.pages.lastOrNull { it.data.isNotEmpty() }
?.data?.lastOrNull()
?.let { db.remoteKeyDao().keyFor(it.id) }
}
// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)التخزين المؤقت وحداثة البيانات
بما أن البيانات موجودة في Room، يحمّل التطبيق فورًا ويعمل دون اتصال. ولتجنب المحتوى القديم، أعد تعريف initialize() لتحديد ما إذا كان يجب التحديث عند التشغيل استنادًا إلى عمر البيانات المخزّنة مؤقتًا.
override suspend fun initialize(): InitializeAction {
val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
val cacheTimeout = TimeUnit.HOURS.toMillis(1)
return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
InitializeAction.LAUNCH_INITIAL_REFRESH
} else {
InitializeAction.SKIP_INITIAL_REFRESH
}
}تحقق سريع
في إعداد يستخدم RemoteMediator، ما المصدر الوحيد للحقيقة الذي تحمّل منه واجهة المستخدم الصفحات؟
مراجعة: RemoteMediator والتخزين المؤقت
لقد بنيت مسارًا لتحميل الصفحات مع أولوية عدم الاتصال:
- تحمّل واجهة المستخدم الصفحات من
PagingSourceتابع لـ Room، وهو المصدر الوحيد للحقيقة - تتعامل
RemoteMediator.load()معREFRESH/PREPEND/APPENDوتكتب النتائج في Room داخل معاملة - يحفظ جدول المفاتيح البعيدة مفاتيح الصفحات التالية والسابقة بعد إعادة التشغيل
- يستقبل
Pagerكلًا منremoteMediatorوpagingSourceFactory - تتحكم
initialize()في حداثة البيانات المخزّنة مؤقتًا
يمكنك الآن تحميل القوائم اللانهائية بكفاءة، سواء كنت متصلًا بالإنترنت أم لا. تهانينا على إكمال دورة Paging 3.
تعلم Kotlin مع معلم ذكاء اصطناعي — مجانًا
اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.
- الدورات
- 36
- الدروس
- 152
الأسئلة الشائعة
هل درس «RemoteMediator والتخزين المؤقت» مجاني؟
نعم — نص درس «RemoteMediator والتخزين المؤقت» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 4 دروس في المجموع.
ماذا ستتعلم في «RemoteMediator والتخزين المؤقت»؟
اجمع بين Paging الشبكة وقاعدة البيانات تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟
لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «RemoteMediator والتخزين المؤقت»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟
نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- لماذا نستخدم Paging
- PagingSource وPager
- Paging في قوائم Compose
- RemoteMediator والتخزين المؤقت