توفر مكتبة تسجيل الصفحات إمكانات قوية لتحميل وعرض البيانات المقسّمة إلى صفحات من مجموعة بيانات أكبر. يوضِّح هذا الدليل كيفية استخدام مكتبة "تتبُّع الصفحات" لإعداد تدفق من البيانات المقسّمة على صفحات من مصدر بيانات شبكة وعرضها في RecyclerView
.
تحديد مصدر بيانات
تتمثّل الخطوة الأولى في تحديد عملية تنفيذ
PagingSource
لتحديد مصدر البيانات. تتضمّن فئة واجهة برمجة التطبيقات PagingSource
الطريقة
load()
التي تلغيها للإشارة إلى كيفية استرداد البيانات المقسّمة إلى صفحات من
مصدر البيانات المقابل.
استخدِم الفئة PagingSource
مباشرةً لاستخدام الكوروتينات في لغة Kotlin للتحميل غير المتزامن. توفر مكتبة تسجيل الصفحات أيضًا فئات لدعم الأطر
الأخرى غير المتزامنة:
- لاستخدام RxJava، قم بتنفيذ
RxPagingSource
بدلاً من ذلك. - لاستخدام
ListenableFuture
من Guava، نفِّذListenableFuturePagingSource
بدلاً من ذلك.
اختيار أنواع المفتاح والقيمة
تتضمّن PagingSource<Key, Value>
مَعلمة نوع: Key
وValue
. يحدد المفتاح المعرف المستخدم لتحميل البيانات، والقيمة هي نوع البيانات نفسها. على سبيل المثال، إذا حمّلت صفحات من كائنات User
من الشبكة عن طريق تمرير أرقام Int
إلى التعديل، اختَر Int
كنوع Key
وUser
كنوع Value
.
تحديد مصدر الترحيل
ينفِّذ المثال التالي سمة 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);
لمزيد من المعلومات عن معالجة أخطاء التعديل، يُرجى الاطّلاع على النماذج في
مرجع واجهة برمجة التطبيقات PagingSource
.
يجمع PagingSource
كائنات LoadResult.Error
ويقدمها إلى واجهة المستخدم حتى تتمكن من اتخاذ إجراءات بشأنها. لمزيد من المعلومات حول عرض حالة التحميل في واجهة المستخدم، يمكنك الاطّلاع على إدارة حالات التحميل وعرضها.
إعداد ساحة مشاركات لبيانات التنقل
بعد ذلك، ستحتاج إلى مصدر بيانات مقسّمة على صفحات من تنفيذ PagingSource
.
عليك إعداد مصدر البيانات في ViewModel
. توفّر الفئة
Pager
طرقًا
تعرض بثًا تفاعليًا لعناصر
PagingData
من
PagingSource
. تتيح مكتبة تسجيل الصفحات استخدام عدة أنواع من مجموعات البث،
منها Flow
وLiveData
وFlowable
وObservable
من
RxJava.
عند إنشاء مثيل Pager
لإعداد البث التفاعلي، يجب
تزويد المثيل بكائن ضبط
PagingConfig
ودالة توضّح طريقة الحصول على مثيل
من تنفيذ PagingSource
:Pager
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()
مصدر البيانات قابلاً للمشاركة ويخزِّن البيانات المحمَّلة مؤقتًا باستخدام CoroutineScope
المقدَّمة. يستخدم هذا المثال السمة viewModelScope
المتوفّرة من خلال عنصر lifecycle-viewmodel-ktx
لدورة الحياة.
يستدعي الكائن Pager
الطريقة load()
من الكائن PagingSource
، ويقدّمه بالكائن
LoadParams
،
ويحصل على الكائن
LoadResult
في المقابل.
تحديد محوّل RecyclerView
يجب أيضًا إعداد محوّل لاستلام البيانات في قائمة
RecyclerView
الخاصة بك. وتوفّر "مكتبة تسجيل الصفحات" فئة 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
:
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
الآن البيانات المقسّمة على صفحات من مصدر البيانات وتحمِّل صفحة أخرى تلقائيًا عند الضرورة.
مراجع إضافية
لمعرفة المزيد من المعلومات حول مكتبة تسجيل الصفحات، اطّلِع على المراجع الإضافية التالية:
الدروس التطبيقية حول الترميز
عيّنات
- نموذج الانتقال إلى صفحات للمكوّنات الهندسية لنظام التشغيل Android
- نموذج ترحيل مكونات البنية الأساسية لنظام التشغيل Android باستخدام الشبكة
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عند إيقاف JavaScript.
- صفحة من الشبكة وقاعدة البيانات
- نقل البيانات إلى الصفحة 3
- نظرة عامة على مكتبة نقل الصفحات