يمكنك تقديم تجربة محسّنة للمستخدم من خلال التأكّد من إمكانية استخدام التطبيق عندما تكون اتصالات الشبكة غير موثوقة أو عندما يكون المستخدِم غير متصل بالإنترنت. تتمثل إحدى طرق القيام بذلك في الانتقال إلى صفحة من الشبكة ومن قاعدة بيانات محلية في نفس الوقت. وبهذه الطريقة، ينقل تطبيقك واجهة المستخدم من ذاكرة التخزين المؤقت لقاعدة البيانات المحلية ولا يُجري الطلبات إلا على الشبكة عندما لا يكون هناك مزيد من البيانات في قاعدة البيانات.
يفترض هذا الدليل أنك على دراية بـ مكتبة استمرارية الغرف بالإضافة إلى الاستخدام الأساسي لمكتبة "تسجيل الصفحات".
تنسيق عمليات تحميل البيانات
توفّر مكتبة تسجيل الصفحات المكوِّن
RemoteMediator
لحالة الاستخدام هذه. يعمل RemoteMediator
كإشارة من مكتبة تسجيل الصفحات عند نفاد
البيانات المخزَّنة مؤقتًا في التطبيق. يمكنك استخدام هذه الإشارة لتحميل بيانات إضافية من الشبكة وتخزينها في قاعدة البيانات المحلية، حيث يمكن لـ
PagingSource
تحميلها وتقديمها إلى واجهة المستخدم لعرضها.
عند الحاجة إلى بيانات إضافية، تستدعي "مكتبة تسجيل الصفحات" الطريقة
load()
من
عملية تنفيذ RemoteMediator
. هذه دالة تعليق، لذا فهي آمنة
لأداء العمل الطويل الأمد. عادةً ما تجلب هذه الدالة البيانات الجديدة
من مصدر شبكة وتحفظها في التخزين المحلي.
تعمل هذه العملية مع البيانات الجديدة، ولكن بمرور الوقت، تتطلّب البيانات المخزّنة في قاعدة البيانات إبطالًا، مثل ما يحدث عندما يشغّل المستخدم عملية التحديث يدويًا. ويتم تمثيل ذلك من خلال السمة LoadType
التي يتم تمريرها إلى طريقة load()
. وتخبر السمة LoadType
RemoteMediator
ما إذا كان يجب إعادة تحميل البيانات الحالية أو جلب
بيانات إضافية يجب إلحاقها بالقائمة الحالية أو إلحاقها بها.
بهذه الطريقة، تضمن RemoteMediator
تحميل تطبيقك للبيانات التي يريد المستخدمون الاطّلاع عليها بالترتيب المناسب.
مراحل النشاط
![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-base-lifecycle.png?authuser=5&hl=ar)
عند نقل البيانات مباشرةً من الشبكة، تُحمِّل PagingSource
البيانات وتعرض كائن LoadResult
. يتم تمرير عملية تنفيذ PagingSource
إلى
Pager
من خلال
المَعلمة pagingSourceFactory
.
بما أنّ واجهة المستخدم تطلب البيانات الجديدة، تستدعي Pager
الطريقة
load()
من
PagingSource
وتعرض تدفق عناصر
PagingData
التي تضم البيانات الجديدة. يتم عادةً تخزين كل عنصر PagingData
مؤقتًا في
ViewModel
قبل إرساله إلى واجهة المستخدم لعرضه.
![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-layered-lifecycle.png?authuser=5&hl=ar)
يؤدي RemoteMediator
إلى تغيير تدفق البيانات. تواصل PagingSource
تحميل البيانات،
ولكن عند نفاد البيانات المقسّمة على صفحات، تشغِّل "مكتبة الصفحات" RemoteMediator
لتحميل بيانات جديدة من مصدر الشبكة. تخزِّن السمة RemoteMediator
البيانات الجديدة في قاعدة البيانات المحلية، وبالتالي لا حاجة إلى استخدام ذاكرة تخزين مؤقت في الذاكرة في ViewModel
. وأخيرًا، تُبطل PagingSource
نفسها، وتنشئ Pager
مثيلاً جديدًا لتحميل البيانات الجديدة من قاعدة البيانات.
الاستخدام الأساسي
لنفترض أنّك تريد من تطبيقك تحميل صفحات من User
عنصر من مصدر بيانات شبكة مستند إلى عنصر إلى ذاكرة تخزين مؤقت محلية تم تخزينها في قاعدة بيانات غرفة.
يساعد تنفيذ RemoteMediator
في تحميل البيانات المقسّمة إلى صفحات من الشبكة إلى قاعدة البيانات، ولكن لا يحمِّل البيانات مباشرةً إلى واجهة المستخدم. بدلاً من ذلك، يستخدم التطبيق
قاعدة البيانات باعتبارها مصدر
الحقيقة. بمعنى آخر، لا يعرض التطبيق سوى
البيانات التي تم تخزينها مؤقتًا في قاعدة البيانات. يعالج تنفيذ PagingSource
(على سبيل المثال، الذي تم إنشاؤه بواسطة Room) تحميل البيانات المخزّنة مؤقتًا من قاعدة البيانات إلى واجهة المستخدم.
إنشاء عناصر الغرفة
الخطوة الأولى هي استخدام مكتبة استمرارية الغرفة لتحديد قاعدة بيانات تحتوي على ذاكرة تخزين مؤقت محلية للبيانات المقسّمة على صفحات من مصدر بيانات الشبكة. يمكنك البدء
بتنفيذ RoomDatabase
كما هو موضّح في حفظ البيانات في قاعدة بيانات محلية باستخدام
Room.
بعد ذلك، حدِّد عنصر غرفة لتمثيل جدول عناصر القائمة كما هو موضّح في
تحديد البيانات باستخدام كيانات الغرفة.
امنحه الحقل id
كمفتاح أساسي، بالإضافة إلى حقول لأي
معلومات أخرى تحتوي عليها عناصر قائمتك.
Kotlin
@Entity(tableName = "users") data class User(val id: String, val label: String)
Java
@Entity(tableName = "users") public class User { public String id; public String label; }
Java
@Entity(tableName = "users") public class User { public String id; public String label; }
عليك أيضًا تعريف عنصر الوصول إلى البيانات (DAO) لكيان الغرفة هذا على النحو الموضَّح في الوصول إلى البيانات باستخدام عناصر DAO للغرفة. يجب أن يتضمن DAO لكيان عنصر القائمة الطرق التالية:
- طريقة
insertAll()
لإدراج قائمة بالعناصر في الجدول. - يشير ذلك المصطلح إلى طريقة تستخدم سلسلة طلب البحث كمَعلمة وتعرض العنصر
PagingSource
لقائمة النتائج. بهذه الطريقة، يمكن لكائنPager
استخدام هذا الجدول كمصدر للبيانات المقسّمة على صفحات. - طريقة
clearAll()
تحذف جميع بيانات الجدول.
Kotlin
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(users: List<User>) @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> @Query("DELETE FROM users") suspend fun clearAll() }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
تنفيذ RemoteMediator
ويتمثل الدور الرئيسي لـ RemoteMediator
في تحميل المزيد من البيانات من الشبكة عند نفاد بيانات Pager
أو عند عدم صلاحية البيانات الحالية. ويتضمّن
طريقة load()
يجب تجاوزها لتحديد سلوك
التحميل.
يتضمّن تنفيذ RemoteMediator
النموذجي المَعلمات التالية:
query
: سلسلة طلب بحث تحدّد البيانات المطلوب استردادها من خدمة الخلفية.database
: قاعدة بيانات الغرفة التي يتم استخدامها كذاكرة تخزين مؤقت محلية.networkService
: مثيل واجهة برمجة التطبيقات لخدمة الخلفية
أنشِئ عملية تنفيذ RemoteMediator<Key, Value>
. يجب أن يكون النوع Key
والنوع Value
متطابقَين كما لو كنت تُعرِّف PagingSource
مقابل مصدر بيانات الشبكة نفسه. لمزيد من المعلومات حول اختيار نوع المعلمات، راجع اختيار أنواع المفتاح والقيمة.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { // ... } }
Java
@UseExperimental(markerClass = ExperimentalPagingApi.class) class ExampleRemoteMediator extends RxRemoteMediator<Integer, User> { private String query; private ExampleBackendService networkService; private RoomDb database; private UserDao userDao; ExampleRemoteMediator( String query, ExampleBackendService networkService, RoomDb database ) { query = query; networkService = networkService; database = database; userDao = database.userDao(); } @NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { ... } }
Java
class ExampleRemoteMediator extends ListenableFutureRemoteMediator<Integer, User> { private String query; private ExampleBackendService networkService; private RoomDb database; private UserDao userDao; private Executor bgExecutor; ExampleRemoteMediator( String query, ExampleBackendService networkService, RoomDb database, Executor bgExecutor ) { this.query = query; this.networkService = networkService; this.database = database; this.userDao = database.userDao(); this.bgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { ... } }
تكون الطريقة load()
مسؤولة عن تعديل مجموعة بيانات النسخ الاحتياطي وإبطال PagingSource
. ستتعامل بعض المكتبات التي تتيح عملية نقل البيانات (مثل Room)
تلقائيًا مع إلغاء صلاحية عناصر PagingSource
التي
تنفّذها.
تستخدم الطريقة load()
معلَمتين:
PagingState
، الذي يحتوي على معلومات حول الصفحات التي تمّ تحميلها حتى الآن، وأحدث فهرس تم الوصول إليه، وكائنPagingConfig
الذي استخدمته لإعداد ساحة مشاركات الصفحات.LoadType
، التي تشير إلى نوع التحميل:REFRESH
أوAPPEND
أوPREPEND
.
والقيمة المعروضة لطريقة load()
هي كائن
MediatorResult
. يمكن أن تكون السمة MediatorResult
إما
MediatorResult.Error
(بما في ذلك وصف الخطأ) أو
MediatorResult.Success
(التي تتضمن إشارة توضح ما إذا كان هناك المزيد من البيانات ليتم تحميلها أم لا).
يجب أن تنفذ طريقة load()
الخطوات التالية:
- حدِّد الصفحة المطلوب تحميلها من الشبكة استنادًا إلى نوع التحميل والبيانات التي تم تحميلها حتى الآن.
- شغِّل طلب الشبكة.
- نفِّذ الإجراءات استنادًا إلى نتيجة عملية التحميل:
- إذا كان التحميل ناجحًا ولم تكن قائمة العناصر التي تم استلامها فارغة،
يُرجى تخزين عناصر القائمة في قاعدة البيانات وعرض
MediatorResult.Success(endOfPaginationReached = false)
. بعد تخزين البيانات، عليك إلغاء صلاحية مصدر البيانات لإبلاغ مكتبة تسجيل الصفحات بالبيانات الجديدة. - إذا كان التحميل ناجحًا وكانت قائمة العناصر التي تم استلامها فارغة
أو كانت فهرس الصفحة الأخير، يتم عرض
MediatorResult.Success(endOfPaginationReached = true)
. بعد تخزين البيانات، عليك إلغاء صلاحية مصدر البيانات لإبلاغ مكتبة تسجيل الصفحات بالبيانات الجديدة. - إذا كان الطلب يتسبب في حدوث خطأ، يجب عرض
MediatorResult.Error
.
- إذا كان التحميل ناجحًا ولم تكن قائمة العناصر التي تم استلامها فارغة،
يُرجى تخزين عناصر القائمة في قاعدة البيانات وعرض
Kotlin
override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional after=<user.id> // parameter. For every page after the first, pass the last user // ID to let it continue from where it left off. For REFRESH, // pass null to load the first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true) LoadType.APPEND -> { val lastItem = state.lastItemOrNull() // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to be // wrapped in a withContext(Dispatcher.IO) { ... } block since // Retrofit's Coroutine CallAdapter dispatches on a worker // thread. val response = networkService.searchUsers( query = query, after = loadKey ) database.withTransaction { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query) } // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } }
Java
@NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. String loadKey = null; switch (loadType) { case REFRESH: break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Single.just(new MediatorResult.Success(true)); } loadKey = lastItem.getId(); break; } return networkService.searchUsers(query, loadKey) .subscribeOn(Schedulers.io()) .map((Function<SearchUserResponse, MediatorResult>) response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. String loadKey = null; switch (loadType) { case REFRESH: break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } loadKey = lastItem.getId(); break; } ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, loadKey), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }
تحديد طريقة الإعداد
يمكن لعمليات تنفيذ RemoteMediator
أيضًا إلغاء طريقة
initialize()
للتحقق مما إذا كانت البيانات المخزَّنة مؤقتًا قديمة وتحديد ما إذا كان سيتم تفعيل
إعادة التحميل عن بُعد. وتعمل هذه الطريقة قبل إجراء أي تحميل، لذلك يمكنك معالجة قاعدة البيانات (على سبيل المثال، لمحو البيانات القديمة) قبل تشغيل أي عمليات تحميل محلية أو بعيدة.
بما أنّ initialize()
هي دالة غير متزامنة، يمكنك تحميل البيانات لتحديد مدى صلة البيانات الحالية في قاعدة البيانات بموضوع البحث. تتمثل الحالة الأكثر شيوعًا
في أن البيانات المخزنة مؤقتًا تكون صالحة فقط لفترة زمنية معينة. يمكن
للRemoteMediator
التحقُّق ممّا إذا كان وقت انتهاء الصلاحية هذا قد انقضى، وفي هذه الحالة
تحتاج مكتبة تسجيل الصفحات إلى إعادة تحميل البيانات بالكامل. ينبغي أن تعرض عمليات تنفيذ
initialize()
عنصر InitializeAction
على النحو التالي:
- وفي الحالات التي يجب فيها إعادة تحميل البيانات المحلية بالكامل، من المفترض أن تعرض السمة
initialize()
InitializeAction.LAUNCH_INITIAL_REFRESH
. ويؤدي ذلك إلى إجراءRemoteMediator
إعادة تحميل عن بُعد لإعادة تحميل البيانات بالكامل. أي عمليات تحميلAPPEND
أوPREPEND
عن بُعد تنتظر نجاح تحميلREFRESH
قبل المتابعة. - في الحالات التي لا تتطلّب إعادة تحميل البيانات المحلّية، من المفترض أن تعرض السمة
initialize()
InitializeAction.SKIP_INITIAL_REFRESH
. ويؤدي ذلك إلى تخطّيRemoteMediator
عملية إعادة التحميل عن بُعد وتحميل بيانات ذاكرة التخزين المؤقت.
Kotlin
override suspend fun initialize(): InitializeAction { val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) return if (System.currentTimeMillis() - db.lastUpdated() <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. InitializeAction.SKIP_INITIAL_REFRESH } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. InitializeAction.LAUNCH_INITIAL_REFRESH } }
Java
@NotNull @Override public Single<InitializeAction> initializeSingle() { long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); return mUserDao.lastUpdatedSingle() .map(lastUpdatedMillis -> { if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. return InitializeAction.SKIP_INITIAL_REFRESH; } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. return InitializeAction.LAUNCH_INITIAL_REFRESH; } }); }
Java
@NotNull @Override public ListenableFuture<InitializeAction> initializeFuture() { long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); return Futures.transform( mUserDao.lastUpdated(), lastUpdatedMillis -> { if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. return InitializeAction.SKIP_INITIAL_REFRESH; } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. return InitializeAction.LAUNCH_INITIAL_REFRESH; } }, mBgExecutor); }
إنشاء جهاز نداء
أخيرًا، عليك إنشاء مثيل Pager
لإعداد مصدر البيانات المقسّمة على صفحات.
يشبه ذلك إنشاء Pager
من مصدر بيانات بسيط للشبكة، ولكن هناك إجراءان عليك تنفيذهما بشكل مختلف:
- بدلاً من تمرير الدالة الإنشائية
PagingSource
مباشرةً، عليك توفير طريقة الاستعلام التي تعرض كائنPagingSource
من DAO. - يجب توفير مثيل من تنفيذ
RemoteMediator
على أنّه المَعلمةremoteMediator
.
Kotlin
val userDao = database.userDao() val pager = Pager( config = PagingConfig(pageSize = 50) remoteMediator = ExampleRemoteMediator(query, database, networkService) ) { userDao.pagingSource(query) }
Java
UserDao userDao = database.userDao(); Pager<Integer, User> pager = Pager( new PagingConfig(/* pageSize = */ 20), null, // initialKey, new ExampleRemoteMediator(query, database, networkService) () -> userDao.pagingSource(query));
Java
UserDao userDao = database.userDao(); Pager<Integer, User> pager = Pager( new PagingConfig(/* pageSize = */ 20), null, // initialKey new ExampleRemoteMediator(query, database, networkService, bgExecutor), () -> userDao.pagingSource(query));
التعامل مع حالات السباق
إن أحد الحالات التي يحتاج تطبيقك إلى التعامل معها عند تحميل البيانات من مصادر متعددة هو الحالة التي تصبح فيها البيانات المخزنة مؤقتًا المحلية غير متزامنة مع مصدر البيانات البعيد.
عندما تعرض طريقة initialize()
من تنفيذ RemoteMediator
الخطأ
LAUNCH_INITIAL_REFRESH
، تكون البيانات قديمة ويجب استبدالها ببيانات جديدة. يتم فرض الانتظار على أي طلبات تحميل PREPEND
أو APPEND
إلى أن يتم نجاح تحميل REFRESH
عن بُعد. بما أنّه تم وضع طلبات PREPEND
أو APPEND
في "قائمة المحتوى التالي" قبل طلب REFRESH
، من المحتمل أن تصبح طلبات PagingState
التي تم تمريرها إلى عمليات التحميل هذه قديمة بحلول وقت تنفيذها.
وبناءً على كيفية تخزين البيانات محليًا، يمكن لتطبيقك تجاهل الطلبات المكرّرة
إذا تسببت التغييرات في البيانات المخزّنة مؤقتًا في إبطال صلاحية البيانات واسترجاع بيانات جديدة.
على سبيل المثال، تؤدي الغرفة إلى إلغاء طلبات البحث في أي عملية إدراج للبيانات. وهذا يعني أنّه يتم توفير عناصر
PagingSource
الجديدة التي تتضمّن البيانات التي تمّت إعادة تحميلها إلى طلبات التحميل
المعلّقة عند إدراج بيانات جديدة في قاعدة البيانات.
يعد حل مشكلة مزامنة البيانات هذه أمرًا ضروريًا لضمان رؤية المستخدمين للبيانات الأكثر صلة وحداثة. يعتمد أفضل حل في الغالب على الطريقة التي تربط بها مصدر بيانات الشبكة البيانات. في جميع الأحوال، تسمح لك المفاتيح عن بُعد بحفظ معلومات عن أحدث صفحة تم طلبها من الخادم. يمكنك للتطبيق استخدام هذه المعلومات لتحديد صفحة البيانات الصحيحة وطلب تحميلها بعد ذلك.
إدارة المفاتيح عن بُعد
المفاتيح عن بُعد هي مفاتيح يستخدمها تطبيق RemoteMediator
لإبلاغ خدمة الخلفية بالبيانات المطلوب تحميلها بعد ذلك. في أبسط الحالات، يتضمن كل عنصر من البيانات
المقسمة على صفحات مفتاح بعيد يمكنك الرجوع إليه بسهولة. ومع ذلك، إذا كانت المفاتيح عن بُعد لا تتوافق مع عناصر فردية، يجب تخزينها
بشكل منفصل وإدارتها في طريقة load()
.
يوضّح هذا القسم طريقة جمع وتخزين وتعديل مفاتيح التحكّم عن بُعد غير المخزّنة في عناصر فردية.
مفاتيح العناصر
يصف هذا القسم طريقة العمل باستخدام المفاتيح عن بُعد التي تتوافق مع
عناصر فردية. وعادةً، عند إيقاف عناصر واجهة برمجة التطبيقات عن عناصر فردية، يتم تمرير معرّف العنصر كمَعلمة طلب بحث. يشير اسم المعلمة إلى ما إذا كان يجب أن يستجيب الخادم
بعناصر قبل المعرّف المقدّم أو بعده. في مثال فئة نموذج User
، يُستخدم الحقل id
من الخادم كمفتاح عن بُعد عند طلب بيانات إضافية.
في حال احتياج طريقة load()
إلى إدارة مفاتيح التحكّم عن بُعد الخاصة بالعناصر، تكون هذه المفاتيح عادةً
معرّفات البيانات التي يتم جلبها من الخادم. لا تحتاج عمليات التحديث إلى مفتاح تحميل، لأنها تسترد فقط أحدث البيانات.
وبالمثل، لا تحتاج عمليات الإضافة إلى جلب أي بيانات إضافية لأنّ إعادة التحميل تسحب دائمًا أحدث البيانات من الخادم.
ومع ذلك، تتطلب عمليات الإلحاق معرّفًا. يتطلب هذا منك تحميل العنصر الأخير من قاعدة البيانات واستخدام معرفه لتحميل الصفحة التالية من البيانات. وفي حال عدم وجود عناصر في قاعدة البيانات، يتم ضبط endOfPaginationReached
على "صحيح"، مما يشير إلى الحاجة إلى إعادة تحميل البيانات.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional String // parameter. For every page after the first, pass the String // token returned from the previous page to let it continue // from where it left off. For REFRESH, pass null to load the // first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Get the last User object id for the next RemoteKey. LoadType.APPEND -> { val lastItem = state.lastItemOrNull() // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query) } // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } // End of pagination has been reached if no users are returned from the // service MediatorResult.Success( endOfPaginationReached = response.users.isEmpty() ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
Java
@NotNull @Override public Single>MediatorResult< loadSingle( @NotNull LoadType loadType, @NotNull PagingState>Integer, User< state ) { // The network load method takes an optional String parameter. For every page // after the first, pass the String token returned from the previous page to // let it continue from where it left off. For REFRESH, pass null to load the // first page. Single>String< remoteKeySingle = null; switch (loadType) { case REFRESH: // Initial load should use null as the page key, so you can return null // directly. remoteKeySingle = Single.just(null); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return Single.just(new MediatorResult.Success(true)); } remoteKeySingle = Single.just(lastItem.getId()); break; } return remoteKeySingle .subscribeOn(Schedulers.io()) .flatMap((Function<String, Single<MediatorResult>>) remoteKey -> { return networkService.searchUsers(query, remoteKey) .map(response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getUsers().isEmpty()); }); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. // For every page after the first, pass the last user ID to let it continue // from where it left off. For REFRESH, pass null to load the first page. ResolvableFuture<String> remoteKeyFuture = ResolvableFuture.create(); switch (loadType) { case REFRESH: remoteKeyFuture.set(null); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } remoteKeyFuture.set(lastItem.getId()); break; } return Futures.transformAsync(remoteKeyFuture, remoteKey -> { ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, remoteKey), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getUsers().isEmpty()); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }, bgExecutor); }
مفاتيح الصفحة
يصف هذا القسم طريقة العمل باستخدام مفاتيح التحكم عن بُعد التي لا تتوافق مع عناصر فردية.
إضافة جدول مفتاح التحكّم عن بُعد
عندما لا تكون المفاتيح البعيدة مرتبطة مباشرة بعناصر القائمة، فمن الأفضل تخزينها في جدول منفصل في قاعدة البيانات المحلية. حدد كيان غرفة يمثل جدول مفاتيح عن بُعد:
Kotlin
@Entity(tableName = "remote_keys") data class RemoteKey(val label: String, val nextKey: String?)
Java
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
Java
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
عليك أيضًا تعريف DAO للكيان RemoteKey
:
Kotlin
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(remoteKey: RemoteKey) @Query("SELECT * FROM remote_keys WHERE label = :query") suspend fun remoteKeyByQuery(query: String): RemoteKey @Query("DELETE FROM remote_keys WHERE label = :query") suspend fun deleteByQuery(query: String) }
Java
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertOrReplace(RemoteKey remoteKey); @Query("SELECT * FROM remote_keys WHERE label = :query") Single<RemoteKey> remoteKeyByQuerySingle(String query); @Query("DELETE FROM remote_keys WHERE label = :query") void deleteByQuery(String query); }
Java
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertOrReplace(RemoteKey remoteKey); @Query("SELECT * FROM remote_keys WHERE label = :query") ListenableFuture<RemoteKey> remoteKeyByQueryFuture(String query); @Query("DELETE FROM remote_keys WHERE label = :query") void deleteByQuery(String query); }
التحميل باستخدام مفاتيح التحكم عن بُعد
عندما تحتاج طريقة load()
إلى إدارة مفاتيح الصفحات البعيدة، يجب تحديدها
بشكل مختلف من خلال الطرق التالية مقارنةً بالاستخدام الأساسي لـ
RemoteMediator
:
- يمكنك تضمين موقع إضافي يتضمن مرجعًا إلى DAO لجدول المفاتيح البعيد.
- يمكنك تحديد المفتاح الذي سيتم تحميله تاليًا من خلال إجراء طلب بحث في جدول المفاتيح البعيدة بدلاً من استخدام
PagingState
. - أدرِج المفتاح البعيد الذي تم إرجاعه من مصدر بيانات الشبكة أو خزِّنه بالإضافة إلى البيانات المقسّمة على صفحات نفسها.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() val remoteKeyDao = database.remoteKeyDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional String // parameter. For every page after the first, pass the String // token returned from the previous page to let it continue // from where it left off. For REFRESH, pass null to load the // first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Query remoteKeyDao for the next RemoteKey. LoadType.APPEND -> { val remoteKey = database.withTransaction { remoteKeyDao.remoteKeyByQuery(query) } // You must explicitly check if the page key is null when // appending, since null is only valid for initial load. // If you receive null for APPEND, that means you have // reached the end of pagination and there are no more // items to load. if (remoteKey.nextKey == null) { return MediatorResult.Success( endOfPaginationReached = true ) } remoteKey.nextKey } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { if (loadType == LoadType.REFRESH) { remoteKeyDao.deleteByQuery(query) userDao.deleteByQuery(query) } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace( RemoteKey(query, response.nextKey) ) // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
Java
@NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional String parameter. For every page // after the first, pass the String token returned from the previous page to // let it continue from where it left off. For REFRESH, pass null to load the // first page. Single<RemoteKey> remoteKeySingle = null; switch (loadType) { case REFRESH: // Initial load should use null as the page key, so you can return null // directly. remoteKeySingle = Single.just(new RemoteKey(mQuery, null)); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: // Query remoteKeyDao for the next RemoteKey. remoteKeySingle = mRemoteKeyDao.remoteKeyByQuerySingle(mQuery); break; } return remoteKeySingle .subscribeOn(Schedulers.io()) .flatMap((Function<RemoteKey, Single<MediatorResult>>) remoteKey -> { // You must explicitly check if the page key is null when appending, // since null is only valid for initial load. If you receive null // for APPEND, that means you have reached the end of pagination and // there are no more items to load. if (loadType != REFRESH && remoteKey.getNextKey() == null) { return Single.just(new MediatorResult.Success(true)); } return networkService.searchUsers(query, remoteKey.getNextKey()) .map(response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); remoteKeyDao.deleteByQuery(query); } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey())); // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. ResolvableFuture<RemoteKey> remoteKeyFuture = ResolvableFuture.create(); switch (loadType) { case REFRESH: remoteKeyFuture.set(new RemoteKey(query, null)); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } // Query remoteKeyDao for the next RemoteKey. remoteKeyFuture.setFuture( remoteKeyDao.remoteKeyByQueryFuture(query)); break; } return Futures.transformAsync(remoteKeyFuture, remoteKey -> { // You must explicitly check if the page key is null when appending, // since null is only valid for initial load. If you receive null // for APPEND, that means you have reached the end of pagination and // there are no more items to load. if (loadType != LoadType.REFRESH && remoteKey.getNextKey() == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, remoteKey.getNextKey()), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); remoteKeyDao.deleteByQuery(query); } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey())); // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }, bgExecutor); }
تم تطبيق إعادة التحميل في مكانها.
إذا كان تطبيقك يحتاج فقط إلى إتاحة عمليات إعادة تحميل الشبكة من أعلى القائمة كما
في الأمثلة السابقة، لن يحتاج RemoteMediator
إلى تحديد سلوك
التحميل في البداية.
ومع ذلك، إذا كان تطبيقك يحتاج إلى إتاحة التحميل التدريجي من الشبكة إلى قاعدة البيانات المحلية، عليك تقديم دعم لاستئناف عملية التقسيم على صفحات بدءًا من نقطة الارتساء، وهي موضع تمرير المستخدِم. تعالج عملية تنفيذ PagingSource
في الغرفة هذا الإجراء نيابةً عنك، ولكن في حال عدم استخدام Room، يمكنك إجراء ذلك
من خلال تجاوز
PagingSource.getRefreshKey()
.
للاطّلاع على مثال لتنفيذ دالة getRefreshKey()
، راجِع تحديد مصدر الصفحات.
يوضح الشكل 4 عملية تحميل البيانات أولاً من قاعدة البيانات المحلية، ثم من الشبكة بمجرد خروج قاعدة البيانات من البيانات.
مراجع إضافية
لمعرفة المزيد من المعلومات حول مكتبة تسجيل الصفحات، اطّلِع على المراجع الإضافية التالية:
الدروس التطبيقية حول الترميز
عيّنات
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عند إيقاف JavaScript.
- تحميل البيانات المقسّمة على صفحات وعرضها
- اختبار تنفيذ عملية الترحيل
- نقل البيانات إلى الصفحة 3