توفّر مكتبة Paging إمكانات قوية لتحميل بيانات مُقسّمة وعرضها من مجموعة بيانات أكبر. يوضّح هذا الدليل كيفية استخدام مكتبة Paging
لإعداد بث للبيانات المفصّلة من مصدر بيانات على الشبكة وعرض
هذه البيانات في RecyclerView.
تحديد مصدر بيانات
الخطوة الأولى هي تحديد عملية تنفيذ
PagingSource
لتحديد مصدر البيانات. تتضمّن فئة PagingSource API الطريقة
load()
، والتي يمكنك إلغاء تحديدها للإشارة إلى كيفية استرداد البيانات المعروضة على صفحات من
مصدر البيانات المقابل.
استخدِم فئة PagingSource مباشرةً لاستخدام مهام Kotlin المتعدّدة المهام لتحميل المحتوى بشكل غير متزامن. توفّر مكتبة Paging أيضًا فئات لتتوافق مع إطارات العمل الأخرى التي تعمل بلا إنترنت:
- لاستخدام RxJava، يمكنك تنفيذ
RxPagingSourceبدلاً من ذلك. - لاستخدام
ListenableFutureمن Guava، يمكنك تنفيذListenableFuturePagingSourceبدلاً من ذلك.
اختيار أنواع المفاتيح والقيم
يحتوي PagingSource<Key, Value> على مَعلمتَي نوع: Key وValue. يحدِّد المفتاح
المعرِّف المستخدَم لتحميل البيانات، والقيمة هي نوع
البيانات نفسها. على سبيل المثال، إذا حمّلت صفحات من عناصر User من الشبكة
من خلال تمرير أرقام صفحات Int إلى
Retrofit،
اختَر Int كنوع Key وUser كنوع Value.
تحديد PagingSource
ينفِّذ المثال التالي PagingSource الذي يحمِّل صفحات المنتجات حسب رقم الصفحة. نوع Key هو Int ونوع Value هو
User.
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error for // expected errors (such as a network failure). } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUserResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) { return new LoadResult.Page<>(response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
يُرسِل تنفيذ PagingSource النموذجي المَعلمات المقدَّمة في
الناسخ إلى طريقة load() لتحميل البيانات المناسبة للاستعلام. في المثال
أعلاه، تكون هذه المَعلمات هي:
-
backend: مثيل لخدمة الخلفية التي تقدّم البيانات query: طلب البحث المطلوب إرساله إلى الخدمة الموضّحة فيbackend
يحتوي العنصر LoadParams
على معلومات عن عملية التحميل التي سيتم تنفيذها. ويشمل ذلك
المفتاح الذي سيتم تحميله وعدد العناصر التي سيتم تحميلها.
يحتوي العنصر LoadResult
على نتيجة عملية التحميل. LoadResult هي فئة مختومة
تتّخذ أحد الشكلَين التاليَين، استنادًا إلى ما إذا كان قد تمّ استدعاء load() بنجاح:
- في حال اكتمال التحميل بنجاح، يجب عرض عنصر
LoadResult.Page. - إذا لم يتم تحميل البيانات بنجاح، يجب عرض عنصر
LoadResult.Error.
يوضّح الشكل التالي كيفية تلقّي الدالة load() في هذا المثال
للمفتاح لكل عملية تحميل وتقديم المفتاح لعملية التحميل اللاحقة.
load() للمفتاح وتعديله
يجب أن ينفّذ تنفيذ PagingSource أيضًا getRefreshKey()
طريقة تأخذ كائن PagingState كمعلّمة. تُعيد هذه الدالة المفتاح الذي يتم تمريره إلى طريقة load() عند تحديث البيانات أو إلغاء صلاحيتها بعد التحميل الأولي. تستدعي "مكتبة الفهرسة" هذه المحاولة تلقائيًا عند إعادة تحميل البيانات لاحقًا.
معالجة الأخطاء
يمكن أن يتعذّر إكمال طلبات تحميل البيانات لعدد من الأسباب، خاصةً عند التحميل
عبر شبكة. أبلِغ عن الأخطاء التي تحدث أثناء التحميل من خلال عرض كائن
LoadResult.Error من الطريقة load().
على سبيل المثال، يمكنك رصد أخطاء التحميل في ExamplePagingSource
من المثال السابق والإبلاغ عنها عن طريق إضافة ما يلي إلى طريقة load():
Kotlin
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
لمزيد من المعلومات عن معالجة أخطاء Retrofit، اطّلِع على العيّنات في
PagingSource مرجع واجهة برمجة التطبيقات.
تجمع PagingSource عناصر LoadResult.Error وترسلها إلى واجهة المستخدم كي تتمكّن من اتّخاذ إجراء بشأنها. لمزيد من المعلومات عن عرض حالة التحميل
في واجهة المستخدم، يُرجى الاطّلاع على مقالة إدارة حالات loading
وعرضها.
إعداد مصدر بيانات PagingData
بعد ذلك، تحتاج إلى مصدر بيانات مُقسّمة إلى صفحات من عملية تنفيذ PagingSource.
اضبط مصدر البيانات في ViewModel. تقدّم فئة
Pager طُرقًا
تعرِض بثًا تفاعليًا لكائنات
PagingData من
PagingSource. تتيح مكتبة Paging استخدام عدة أنواع من مصادر البيانات،
بما في ذلك Flow وLiveData وأنواع Flowable وObservable من
RxJava.
عند إنشاء مثيل Pager لإعداد البث التفاعلي، يجب
تزويد المثيل بملف تكوين
PagingConfig ودالّة تُعلم Pager كيفية الحصول على مثيل من تنفيذ
PagingSource:
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
يجعل عامل التشغيل cachedIn() تدفق البيانات قابلاً للمشاركة ويخزّن مؤقتًا data
المحمَّلة باستخدام CoroutineScope المقدَّمة. يستخدم هذا المثال viewModelScope
الذي يوفّره عنصر lifecycle-viewmodel-ktx الخاص بالرحلة.
يستدعي العنصر Pager الطريقة load() من العنصر PagingSource، ويقدّم له العنصر
LoadParams ويتلقّى العنصر
LoadResult بدوره.
تحديد مهايئ RecyclerView
عليك أيضًا إعداد محوِّل لتلقّي البيانات في قائمة RecyclerView. توفّر مكتبة Paging فئة PagingDataAdapter لهذا
الغرض.
حدِّد فئة تمديد PagingDataAdapter. في المثال،
يُوسّع UserAdapter PagingDataAdapter لتوفير RecyclerView
محوِّل لعناصر القائمة من النوع User واستخدام UserViewHolder كحامل
عرض:
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
يجب أن يحدِّد المحوِّل أيضًا الطريقتَين onCreateViewHolder() و
onBindViewHolder() ويحدِّد
DiffUtil.ItemCallback.
ويعمل هذا الإجراء بالطريقة نفسها التي يعمل بها عادةً عند تحديد محوِّلات RecyclerView list:
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
عرض البيانات المفصّلة في واجهة المستخدم
بعد أن حدّدت PagingSource، واخترت طريقة لتطبيقك ل
إنشاء بث من PagingData، وحدّدت PagingDataAdapter، أصبحت
جاهزًا لربط هذه العناصر معًا وعرض البيانات المفصّلة في
نشاطك.
نفِّذ الخطوات التالية في طريقة onCreate لنشاطك أو
onViewCreated للوحدة:
- أنشئ مثيلًا لفئة
PagingDataAdapter. - نقْل مثيل
PagingDataAdapterإلى قائمةRecyclerViewالتي تريد عرض بياناتك المفصّلة فيها. - راقِب بث
PagingDataوأرسِل كل قيمة تم إنشاؤها إلىsubmitData()في المحوِّل.
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly; fragments use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose. .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly; fragments use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
تعرض قائمة RecyclerView الآن البيانات المفصّلة من مصدر البيانات، وتحميل صفحة أخرى تلقائيًا عند الضرورة.
مصادر إضافية
للاطّلاع على مزيد من المعلومات عن مكتبة Paging، اطّلِع على المراجع الإضافية التالية:
الدروس التطبيقية حول الترميز
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون لغة JavaScript غير مفعّلة.
- الصفحة من الشبكة وقاعدة البيانات
- الانتقال إلى Paging 3
- نظرة عامة على مكتبة الفهرسة