با اطمینان از اینکه برنامه شما می تواند زمانی که اتصالات شبکه قابل اعتماد نیستند یا زمانی که کاربر آفلاین است، قابل استفاده است، تجربه کاربری بهبود یافته ای را ارائه دهید. یکی از راه های انجام این کار، صفحه از شبکه و از یک پایگاه داده محلی به طور همزمان است. به این ترتیب، برنامه شما رابط کاربری را از حافظه نهان پایگاه داده محلی هدایت می کند و تنها زمانی درخواست هایی را به شبکه ارسال می کند که داده دیگری در پایگاه داده وجود نداشته باشد.
این راهنما فرض می کند که شما با کتابخانه ماندگاری اتاق و استفاده اولیه از کتابخانه Paging آشنا هستید.
بارهای داده را هماهنگ کنید
کتابخانه Paging مولفه RemoteMediator
را برای این مورد استفاده می کند. RemoteMediator
به عنوان سیگنالی از کتابخانه Paging عمل می کند وقتی که داده های حافظه پنهان برنامه تمام شود. میتوانید از این سیگنال برای بارگیری دادههای اضافی از شبکه و ذخیره آنها در پایگاه داده محلی استفاده کنید، جایی که یک PagingSource
میتواند آن را بارگیری کند و برای نمایش در اختیار رابط کاربر قرار دهد.
هنگامی که به داده های اضافی نیاز است، کتابخانه Paging متد load()
را از پیاده سازی RemoteMediator
فراخوانی می کند. این یک عملکرد تعلیق است، بنابراین انجام کارهای طولانی مدت ایمن است. این تابع معمولاً داده های جدید را از یک منبع شبکه واکشی می کند و آن را در حافظه محلی ذخیره می کند.
این فرآیند با دادههای جدید کار میکند، اما با گذشت زمان، دادههای ذخیرهشده در پایگاه داده نیاز به نامعتبر شدن دارند، مانند زمانی که کاربر بهطور دستی یک بهروزرسانی را راهاندازی میکند. این ویژگی با ویژگی LoadType
که به متد load()
ارسال می شود نشان داده می شود. LoadType
به RemoteMediator
اطلاع می دهد که آیا باید داده های موجود را بازخوانی کند یا داده های اضافی را که باید به لیست موجود اضافه یا اضافه شود، واکشی کند.
به این ترتیب، RemoteMediator
تضمین می کند که برنامه شما داده هایی را که کاربران می خواهند ببینند به ترتیب مناسب بارگیری می کند.
چرخه عمر صفحه بندی
هنگام صفحهبندی مستقیم از شبکه، PagingSource
دادهها را بارگیری میکند و یک شی LoadResult
برمیگرداند. پیاده سازی PagingSource
از طریق پارامتر pagingSourceFactory
به Pager
ارسال می شود.
از آنجایی که UI به داده های جدیدی نیاز دارد، Pager
متد load()
از PagingSource
فراخوانی می کند و جریانی از اشیاء PagingData
را برمی گرداند که داده های جدید را کپسوله می کند. هر شیء PagingData
معمولاً قبل از ارسال به UI برای نمایش در ViewModel
ذخیره می شود.
RemoteMediator
این جریان داده را تغییر می دهد. یک PagingSource
هنوز داده ها را بارگذاری می کند. اما هنگامی که داده های صفحه بندی شده تمام می شود، کتابخانه Paging RemoteMediator
را برای بارگذاری داده های جدید از منبع شبکه فعال می کند. RemoteMediator
داده های جدید را در پایگاه داده محلی ذخیره می کند، بنابراین یک کش در حافظه در ViewModel
غیر ضروری است. در نهایت، PagingSource
خود را باطل می کند و Pager
یک نمونه جدید برای بارگیری داده های تازه از پایگاه داده ایجاد می کند.
استفاده اساسی
فرض کنید میخواهید برنامه شما صفحاتی از آیتمهای User
را از منبع داده شبکه با کلید مورد در یک حافظه پنهان محلی ذخیره شده در پایگاه داده اتاق بارگیری کند.
پیاده سازی RemoteMediator
به بارگذاری داده های صفحه شده از شبکه در پایگاه داده کمک می کند، اما داده ها را مستقیماً در UI بارگذاری نمی کند. در عوض، برنامه از پایگاه داده به عنوان منبع حقیقت استفاده می کند. به عبارت دیگر، برنامه فقط داده هایی را نمایش می دهد که در پایگاه داده ذخیره شده اند. یک پیاده سازی PagingSource
(به عنوان مثال، یکی که توسط Room ایجاد شده است) بارگذاری داده های کش شده از پایگاه داده در UI را انجام می دهد.
موجودیتهای اتاق را ایجاد کنید
اولین قدم استفاده از کتابخانه تداوم اتاق برای تعریف پایگاه داده ای است که یک حافظه پنهان محلی از داده های صفحه شده از منبع داده شبکه را نگه می دارد. با پیاده سازی RoomDatabase
همانطور که در ذخیره داده ها در پایگاه داده محلی با استفاده از Room توضیح داده شده است شروع کنید.
در مرحله بعد، یک موجودیت اتاق را تعریف کنید تا جدولی از موارد فهرست را نشان دهد، همانطور که در تعریف داده با استفاده از موجودیتهای اتاق توضیح داده شده است. یک فیلد id
به عنوان کلید اصلی و همچنین فیلدهایی برای هر اطلاعات دیگری که آیتمهای فهرست شما حاوی است، به آن بدهید.
کاتلین
@Entity(tableName = "users") data class User(val id: String, val label: String)
جاوا
@Entity(tableName = "users") public class User { public String id; public String label; }
جاوا
@Entity(tableName = "users") public class User { public String id; public String label; }
همچنین باید یک شی دسترسی به داده (DAO) برای این موجودیت اتاق تعریف کنید، همانطور که در دسترسی به داده با استفاده از اتاق DAO توضیح داده شده است. DAO برای موجودیت آیتم لیست باید شامل روش های زیر باشد:
- یک متد
insertAll()
که لیستی از آیتم ها را در جدول قرار می دهد. - متدی که رشته پرس و جو را به عنوان پارامتر می گیرد و یک شی
PagingSource
را برای لیست نتایج برمی گرداند. به این ترتیب، یک شیPager
می تواند از این جدول به عنوان منبع داده های صفحه بندی شده استفاده کند. - یک متد
clearAll()
که تمام داده های جدول را حذف می کند.
کاتلین
@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() }
جاوا
@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(); }
جاوا
@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
: رشته پرس و جو که تعیین می کند کدام داده از سرویس Backend بازیابی شود. -
database
: پایگاه داده اتاق که به عنوان یک کش محلی عمل می کند. -
networkService
: یک نمونه API برای سرویس Backend.
یک پیاده سازی RemoteMediator<Key, Value>
ایجاد کنید. نوع Key
و نوع Value
باید همان باشند که اگر یک PagingSource
در برابر منبع داده شبکه یکسانی تعریف میکردید، یکسان هستند. برای اطلاعات بیشتر در مورد انتخاب پارامترهای نوع، به انتخاب انواع کلید و مقدار مراجعه کنید.
کاتلین
@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 { // ... } }
جاوا
@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 ) { ... } }
جاوا
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
را برگردانید.
- اگر بارگیری موفقیت آمیز بود و لیست موارد دریافتی خالی نبود، موارد لیست را در پایگاه داده ذخیره کنید و
کاتلین
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) } }
جاوا
@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); }); }
جاوا
@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
می تواند بررسی کند که آیا این زمان انقضا گذشته است یا خیر، در این صورت کتابخانه Paging باید داده ها را به طور کامل بازخوانی کند. پیادهسازی initialize()
باید یک InitializeAction
به صورت زیر برگرداند:
- در مواردی که دادههای محلی نیاز به بهروزرسانی کامل دارند،
initialize()
بایدInitializeAction.LAUNCH_INITIAL_REFRESH
برگرداند.LAUNCH_INITIAL_REFRESH. این باعث می شود کهRemoteMediator
یک به روز رسانی از راه دور را برای بارگیری کامل داده ها انجام دهد. هر بارAPPEND
یاPREPEND
از راه دور قبل از ادامه، منتظر می ماند تا بارگیریREFRESH
با موفقیت انجام شود. - در مواردی که دادههای محلی نیازی به رفرش ندارند،
initialize()
بایدInitializeAction.SKIP_INITIAL_REFRESH
برگرداند.SKIP_INITIAL_REFRESH. این باعث می شودRemoteMediator
از رفرش از راه دور صرف نظر کند و داده های کش را بارگیری کند.
کاتلین
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 } }
جاوا
@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; } }); }
جاوا
@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
، باید متد query را ارائه دهید که یک شیPagingSource
را از DAO برمی گرداند. - شما باید یک نمونه از پیاده سازی
RemoteMediator
خود را به عنوان پارامترremoteMediator
ارائه دهید.
کاتلین
val userDao = database.userDao() val pager = Pager( config = PagingConfig(pageSize = 50) remoteMediator = ExampleRemoteMediator(query, database, networkService) ) { userDao.pagingSource(query) }
جاوا
UserDao userDao = database.userDao(); Pager<Integer, User> pager = Pager( new PagingConfig(/* pageSize = */ 20), null, // initialKey, new ExampleRemoteMediator(query, database, networkService) () -> userDao.pagingSource(query));
جاوا
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
که به آن تماسهای بارگذاری ارسال میشود، در زمان اجرای آنها قدیمی باشد.
بسته به نحوه ذخیره دادهها به صورت محلی، برنامه شما میتواند درخواستهای اضافی را نادیده بگیرد اگر تغییرات در دادههای حافظه پنهان باعث بیاعتباری و واکشی دادههای جدید شود. به عنوان مثال، Room درخواستها را در هر درج دادهای باطل میکند. این بدان معناست که اشیاء جدید PagingSource
با دادههای تازهسازی شده به درخواستهای بارگذاری معلق زمانی که دادههای جدید در پایگاه داده درج میشوند، ارائه میشوند.
حل این مشکل همگام سازی داده ها برای اطمینان از اینکه کاربران مرتبط ترین و به روزترین داده ها را می بینند ضروری است. بهترین راه حل بیشتر به روشی بستگی دارد که منبع داده شبکه داده ها را صفحه می کند. در هر صورت، کلیدهای راه دور به شما امکان می دهند اطلاعات مربوط به آخرین صفحه درخواست شده از سرور را ذخیره کنید. برنامه شما میتواند از این اطلاعات برای شناسایی و درخواست صفحه صحیح دادهها برای بارگیری بعدی استفاده کند.
کلیدهای راه دور را مدیریت کنید
کلیدهای راه دور کلیدهایی هستند که پیادهسازی RemoteMediator
از آنها استفاده میکند تا به سرویس پشتیبان بگوید کدام دادهها را بعداً بارگیری کند. در ساده ترین حالت، هر مورد از داده های صفحه بندی شده شامل یک کلید راه دور است که می توانید به راحتی به آن مراجعه کنید. با این حال، اگر کلیدهای راه دور با آیتم های جداگانه مطابقت ندارند، باید آنها را جداگانه ذخیره کنید و آنها را در روش load()
خود مدیریت کنید.
این بخش نحوه جمع آوری، ذخیره و به روز رسانی کلیدهای راه دور را که در موارد جداگانه ذخیره نمی شوند، توضیح می دهد.
کلیدهای مورد
در این بخش نحوه کار با کلیدهای راه دور که با موارد جداگانه مطابقت دارند، توضیح داده می شود. به طور معمول، زمانی که یک API از آیتمهای جداگانه حذف میشود، شناسه مورد به عنوان پارامتر پرس و جو ارسال میشود. نام پارامتر نشان می دهد که آیا سرور باید با موارد قبل یا بعد از شناسه ارائه شده پاسخ دهد. در مثال کلاس User
model، فیلد id
از سرور به عنوان یک کلید راه دور هنگام درخواست داده های اضافی استفاده می شود.
زمانی که متد load()
شما نیاز به مدیریت کلیدهای راه دور مخصوص آیتم دارد، این کلیدها معمولاً شناسه دادههایی هستند که از سرور واکشی میشوند. عملیات تازه سازی نیازی به کلید بارگذاری ندارد، زیرا آنها فقط جدیدترین داده ها را بازیابی می کنند. به طور مشابه، عملیات prepend نیازی به واکشی دادههای اضافی ندارد زیرا Refresh همیشه جدیدترین دادهها را از سرور میکشد.
با این حال، عملیات الحاق به شناسه نیاز دارد. برای این کار باید آخرین مورد را از پایگاه داده بارگیری کنید و از شناسه آن برای بارگیری صفحه بعدی داده استفاده کنید. اگر هیچ موردی در پایگاه داده وجود نداشته باشد، endOfPaginationReached
روی true تنظیم میشود، که نشان میدهد به رفرش داده نیاز است.
کاتلین
@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) } } }
جاوا
@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); }); }
جاوا
@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); }
کلیدهای صفحه
این بخش نحوه کار با کلیدهای راه دوری را که با موارد جداگانه مطابقت ندارند، توضیح می دهد.
جدول کلید از راه دور را اضافه کنید
هنگامی که کلیدهای راه دور مستقیماً با موارد لیست مرتبط نیستند، بهتر است آنها را در یک جدول جداگانه در پایگاه داده محلی ذخیره کنید. یک موجودیت اتاق را تعریف کنید که جدولی از کلیدهای راه دور را نشان می دهد:
کاتلین
@Entity(tableName = "remote_keys") data class RemoteKey(val label: String, val nextKey: String?)
جاوا
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
جاوا
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
همچنین باید یک DAO برای موجودیت RemoteKey
تعریف کنید:
کاتلین
@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) }
جاوا
@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); }
جاوا
@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
، تعیین کنید کدام کلید بارگذاری شود. - کلید راه دور برگشتی را از منبع داده شبکه، علاوه بر خود داده های صفحه، درج یا ذخیره کنید.
کاتلین
@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) } } }
جاوا
@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); }); }
جاوا
@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()
به تعریف منبع Paging مراجعه کنید.
شکل 4 روند بارگیری داده ها را ابتدا از پایگاه داده محلی و سپس از شبکه به محض اینکه پایگاه داده از داده خارج شد را نشان می دهد.
منابع اضافی
برای کسب اطلاعات بیشتر در مورد کتابخانه Paging، به منابع اضافی زیر مراجعه کنید:
Codelabs
نمونه ها
{% کلمه به کلمه %}برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- بارگیری و نمایش داده های صفحه بندی شده
- پیاده سازی Paging خود را تست کنید
- مهاجرت به صفحه 3