توفّر مكتبة 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()
في هذا المثال
للمفتاح لكل عملية تحميل وتقديم المفتاح لعملية التحميل اللاحقة.
يجب أن ينفّذ تنفيذ 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
- نظرة عامة على مكتبة الفهرسة