المفاهيم والتنفيذ في Jetpack Compose
يمكنك تقديم تجربة أفضل للمستخدمين من خلال التأكّد من إمكانية استخدام تطبيقك عندما تكون اتصالات الشبكة غير موثوقة أو عندما يكون المستخدم غير متصل بالإنترنت. ويمكنك إجراء ذلك من خلال تحميل البيانات من الشبكة ومن قاعدة بيانات محلية في الوقت نفسه. بهذه الطريقة، يعرض تطبيقك واجهة المستخدم من ذاكرة تخزين مؤقت لقاعدة بيانات محلية، ولا يرسل طلبات إلى الشبكة إلا عندما لا تتوفّر بيانات أخرى في قاعدة البيانات.
يفترض هذا الدليل أنّك على دراية بمكتبة Room لتخزين البيانات بشكل دائم والاستخدام الأساسي لمكتبة Paging.
تنسيق عمليات تحميل البيانات
توفّر مكتبة Paging المكوّن RemoteMediator لحالة الاستخدام هذه. تعمل RemoteMediator كإشارة من مكتبة Paging
عندما ينتهي التطبيق من البيانات المخزّنة مؤقتًا. يمكنك استخدام هذه الإشارة لتحميل بيانات إضافية من الشبكة وتخزينها في قاعدة البيانات المحلية، حيث يمكن PagingSource تحميلها وتقديمها إلى واجهة المستخدم لعرضها.
عند الحاجة إلى بيانات إضافية، تستدعي مكتبة Paging الدالة
load() من
تنفيذ RemoteMediator. هذه دالة تعليق، لذا يمكن تنفيذ مهام طويلة الأمد بأمان. تسترد هذه الدالة عادةً البيانات الجديدة من مصدر على الشبكة وتحفظها في مساحة التخزين المحلية.
تعمل هذه العملية مع البيانات الجديدة، ولكن بمرور الوقت، تتطلب البيانات المخزَّنة في قاعدة البيانات إبطال الصلاحية، مثلاً عندما يطلب المستخدم إعادة التحميل يدويًا. يتم تمثيل ذلك باستخدام السمة LoadType التي يتم تمريرها إلى الطريقة load(). يُعلم LoadType RemoteMediator ما إذا كان بحاجة إلى إعادة تحميل البيانات الحالية أو جلب بيانات إضافية يجب إلحاقها أو إضافتها قبل القائمة الحالية.
بهذه الطريقة، يضمن RemoteMediator أن يحمّل تطبيقك البيانات التي يريد المستخدمون رؤيتها بالترتيب المناسب.
مراحل النشاط في التقسيم إلى صفحات
عند طلب تقسيم النتائج مباشرةً من الشبكة، يحمّل PagingSource البيانات ويعرض كائن LoadResult. يتم تمرير عملية تنفيذ PagingSource إلى
Pager من خلال
المعلَمة pagingSourceFactory.
بما أنّ واجهة المستخدم تتطلّب بيانات جديدة، يستدعي Pager الطريقة
load() من
PagingSource ويعرض مجموعة من عناصر
PagingData التي
تتضمّن البيانات الجديدة. يتم عادةً تخزين كل عنصر PagingData مؤقتًا في ViewModel قبل إرساله إلى واجهة المستخدم لعرضه.
تؤدي عملية RemoteMediator إلى تغيير تدفّق البيانات هذا. يؤدي PagingSource إلى تحميل البيانات، ولكن عند استنفاد البيانات المقسّمة إلى صفحات، تشغّل مكتبة Paging RemoteMediator لتحميل بيانات جديدة من مصدر الشبكة. يخزّن RemoteMediator البيانات الجديدة في قاعدة البيانات المحلية، لذا لا حاجة إلى ذاكرة تخزين مؤقت داخلية في ViewModel. أخيرًا، يبطل PagingSource صلاحيته، وينشئ Pager مثيلاً جديدًا لتحميل البيانات الجديدة من قاعدة البيانات.
الاستخدام الأساسي
لنفترض أنّك تريد أن يحمّل تطبيقك صفحات تتضمّن User عنصرًا من مصدر بيانات شبكة يستند إلى مفتاح عنصر إلى ذاكرة تخزين مؤقت محلية مخزَّنة في قاعدة بيانات Room.
تساعد عملية تنفيذ RemoteMediator في تحميل البيانات المقسّمة إلى صفحات من الشبكة إلى قاعدة البيانات، ولكنّها لا تحمّل البيانات مباشرةً إلى واجهة المستخدم. وبدلاً من ذلك، يستخدم التطبيق قاعدة البيانات كمصدر
موثوق. بعبارة أخرى، يعرض التطبيق
البيانات التي تم تخزينها مؤقتًا في قاعدة البيانات فقط. تتولّى عملية التنفيذ (على سبيل المثال، تلك التي تم إنشاؤها بواسطة Room) تحميل البيانات المخزّنة مؤقتًا من قاعدة البيانات إلى واجهة المستخدم.PagingSource
إنشاء عناصر Room
تتمثل الخطوة الأولى في استخدام مكتبة Room لتخزين البيانات بشكل دائم من أجل تحديد قاعدة بيانات تحتوي على ذاكرة تخزين مؤقت محلية للبيانات المقسّمة على صفحات من مصدر بيانات الشبكة. ابدأ بتنفيذ RoomDatabase كما هو موضّح في حفظ البيانات في قاعدة بيانات محلية باستخدام Room.
بعد ذلك، حدِّد كيان Room لتمثيل جدول بعناصر القائمة كما هو موضّح في تحديد البيانات باستخدام كيانات Room.
امنحها حقل id كمفتاح أساسي، بالإضافة إلى حقول لأي معلومات أخرى تتضمّنها عناصر القائمة.
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) الخاص بكيان Room هذا كما هو موضّح في الوصول إلى البيانات باستخدام كائنات الوصول إلى البيانات في Room. يجب أن تتضمّن خدمة DAO الخاصة بعنصر القائمة طُرقًا على النحو التالي:
- طريقة
insertAll()تُدرِج قائمة عناصر في الجدول. - طريقة تأخذ سلسلة طلب البحث كمَعلمة وتعرض كائن
PagingSourceلقائمة النتائج. بهذه الطريقة، يمكن أن يستخدم العنصرPagerهذا الجدول كمصدر للبيانات المقسّمة إلى صفحات. - طريقة
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: قاعدة بيانات Room التي تعمل كذاكرة تخزين مؤقت محلية networkService: هي مثيل لواجهة برمجة التطبيقات لخدمة الخلفية.
أنشئ عملية تنفيذ RemoteMediator<Key, Value>. يجب أن يكون النوع Key والنوع Value متطابقَين كما لو كنت تحدّد PagingSource مقابل مصدر بيانات الشبكة نفسه. لمزيد من المعلومات حول اختيار مَعلمات النوع، اطّلِع على اختيار أنواع المفاتيح والقيم.
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). بعد تخزين البيانات، عليك إبطال مصدر البيانات لإعلام مكتبة Paging بالبيانات الجديدة. - في حال نجاح التحميل وكانت قائمة العناصر المستلَمة فارغة أو كانت فهرس الصفحة الأخيرة، يجب عرض
MediatorResult.Success(endOfPaginationReached = true). بعد تخزين البيانات، عليك إبطال مصدر البيانات لإعلام مكتبة Paging بالبيانات الجديدة. - إذا تسبّب الطلب في حدوث خطأ، يجب عرض
MediatorResult.Error.
- إذا تم التحميل بنجاح وكانت قائمة العناصر المستلَمة غير فارغة،
يجب تخزين عناصر القائمة في قاعدة البيانات وعرض
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 ); }
تحديد طريقة initialize
يمكن أن تتجاوز عمليات تنفيذ RemoteMediator أيضًا طريقة
initialize()
للتحقّق مما إذا كانت البيانات المخزّنة مؤقتًا قديمة، وتحديد ما إذا كان سيتم بدء عملية إعادة تحميل عن بُعد. يتم تنفيذ هذه الطريقة قبل إجراء أي عملية تحميل، لذا يمكنك تعديل قاعدة البيانات (على سبيل المثال، لمحو البيانات القديمة) قبل بدء أي عمليات تحميل محلية أو عن بُعد.
بما أنّ initialize() هي دالة غير متزامنة، يمكنك تحميل البيانات لتحديد مدى صلة البيانات الحالية في قاعدة البيانات. الحالة الأكثر شيوعًا هي أنّ البيانات المخزّنة مؤقتًا تكون صالحة لفترة زمنية معيّنة فقط. يمكن RemoteMediator التحقّق مما إذا كان وقت انتهاء الصلاحية هذا قد انقضى، وفي هذه الحالة، يجب أن تعيد مكتبة Paging تحميل البيانات بالكامل. يجب أن تعرض عمليات تنفيذ
initialize() قيمة InitializeAction على النحو التالي:
- في الحالات التي يجب فيها إعادة تحميل البيانات المحلية بالكامل، يجب أن تعرض
initialize()القيمةInitializeAction.LAUNCH_INITIAL_REFRESH. يؤدي ذلك إلى أن تجريRemoteMediatorعملية إعادة تحميل عن بُعد لإعادة تحميل البيانات بالكامل. تنتظر أي عمليات تحميل عن بُعدAPPENDأوPREPENDاكتمال عملية تحميلREFRESHبنجاح قبل المتابعة. - في الحالات التي لا يلزم فيها إعادة تحميل البيانات المحلية، يجب أن تعرض الدالة
initialize()القيمةInitializeAction.SKIP_INITIAL_REFRESH. سيؤدي ذلك إلى أن يتخطىRemoteMediatorعملية إعادة التحميل عن بُعد ويحمّل البيانات المخزّنة مؤقتًا.
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.
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));
إدارة المفاتيح عن بُعد
المفاتيح عن بُعد هي مفاتيح تستخدمها عملية تنفيذ RemoteMediator لإخبار خدمة الخلفية بالبيانات التي يجب تحميلها بعد ذلك. في أبسط الحالات، يتضمّن كل عنصر من البيانات المقسّمة إلى صفحات مفتاحًا عن بُعد يمكنك الرجوع إليه بسهولة. ومع ذلك، إذا لم تتطابق المفاتيح البعيدة مع عناصر فردية، عليك تخزينها بشكل منفصل وإدارتها في طريقة load().
يوضّح هذا القسم كيفية جمع المفاتيح البعيدة وتخزينها وتعديلها، وهي المفاتيح التي لا يتم تخزينها في عناصر فردية.
مفاتيح العناصر
يوضّح هذا القسم كيفية استخدام المفاتيح البعيدة التي تتوافق مع عناصر فردية. عادةً، عندما يتم إيقاف مفتاح واجهة برمجة التطبيقات لعناصر فردية، يتم تمرير رقم تعريف العنصر كمَعلمة طلب بحث. يشير اسم المَعلمة إلى ما إذا كان يجب أن يستجيب الخادم بعناصر قبل المعرّف المقدَّم أو بعده. في مثال فئة النموذج User، يتم استخدام الحقل id من الخادم كمفتاح بعيد عند طلب بيانات إضافية.
عندما تحتاج طريقتك load() إلى إدارة المفاتيح البعيدة الخاصة بالعناصر، تكون هذه المفاتيح عادةً هي معرّفات البيانات التي يتم جلبها من الخادم. لا تحتاج عمليات إعادة التحميل إلى مفتاح تحميل، لأنّها تسترد أحدث البيانات فقط.
وبالمثل، لا تحتاج عمليات الإضافة في المقدّمة إلى جلب أي بيانات إضافية لأنّ عملية إعادة التحميل تجلب دائمًا أحدث البيانات من الخادم.
ومع ذلك، تتطلّب عمليات الإلحاق توفير معرّف. يتطلّب ذلك تحميل العنصر الأخير من قاعدة البيانات واستخدام رقم تعريفه لتحميل الصفحة التالية من البيانات. إذا لم تكن هناك أي عناصر في قاعدة البيانات، سيتم ضبط قيمة endOfPaginationReached على "صحيح"، ما يشير إلى ضرورة إعادة تحميل البيانات.
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); }
مفاتيح الصفحة
يوضّح هذا القسم كيفية استخدام المفاتيح البعيدة التي لا تتوافق مع عناصر فردية.
إضافة جدول المفاتيح عن بُعد
عندما لا تكون المفاتيح البعيدة مرتبطة بشكل مباشر بعناصر القائمة، من الأفضل تخزينها في جدول منفصل في قاعدة البيانات المحلية. عرِّف كيان Room يمثّل جدولاً للمفاتيح البعيدة:
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:
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. - أدرِج المفتاح البعيد الذي تم عرضه أو خزِّنه من مصدر بيانات الشبكة بالإضافة إلى البيانات المقسَّمة إلى صفحات نفسها.
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); }
مراجع إضافية
لمزيد من المعلومات حول مكتبة Paging، يُرجى الاطّلاع على المراجع الإضافية التالية:
الدروس التطبيقية حول الترميز
نماذج
مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة.
- تحميل البيانات المقسّمة إلى صفحات وعرضها
- اختبار تنفيذ مكتبة Paging
- الانتقال إلى مكتبة Paging 3