في أحد اجتماعات قسم Android ، سمعت كيف قام أحد مطورينا بإنشاء ليب صغير يساعد في إعداد قائمة "لا نهاية لها" عند استخدام Realm ، مع الحفاظ على "التحميل الكسول" والإشعارات.
لقد صنعت وكتبت مشروع مقال ، وهو دون تغيير تقريبًا ، أشاركه معك. من جانبه ، وعد بأنه سيشغل المهام ويأتي في تعليقات إذا ظهرت أسئلة.
قائمة لا نهاية لها والحلول الجاهزة
تتمثل إحدى المهام التي نواجهها في عرض المعلومات في قائمة ، عند التمرير خلالها ، يتم تحميل البيانات وإدراجها بشكل غير مرئي للمستخدم. بالنسبة للمستخدم ، يبدو أنه سينتقل إلى قائمة لا حصر لها.
الخوارزمية هي تقريبا ما يلي:
- نحصل على بيانات من ذاكرة التخزين المؤقت للصفحة الأولى ؛
- إذا كانت ذاكرة التخزين المؤقت فارغة - نحصل على بيانات الخادم ، ونعرضها في القائمة والكتابة إلى قاعدة البيانات ؛
- إذا كان هناك ذاكرة تخزين مؤقت ، فقم بتحميلها في القائمة ؛
- إذا وصلنا إلى نهاية قاعدة البيانات ، فإننا نطلب البيانات من الخادم ، ونعرضها في القائمة والكتابة إلى قاعدة البيانات.
المبسطة: لعرض القائمة ، يتم استقصاء ذاكرة التخزين المؤقت لأول مرة ، وتكون الإشارة إلى تحميل بيانات جديدة هي نهاية ذاكرة التخزين المؤقت.
لتنفيذ التمرير الذي لا نهاية له ، يمكنك استخدام حلول جاهزة:
نحن نستخدم
Realm كقاعدة بيانات للهاتف المحمول ، وبعد أن جربنا جميع الأساليب المذكورة أعلاه ، توقفنا عن استخدام مكتبة Paging.
للوهلة الأولى ، تعد Android Paging Library حلاً ممتازًا لتنزيل البيانات ، وعند استخدام sqlite مع Room ، فهي ممتازة كقاعدة بيانات. ومع ذلك ، عند استخدام Realm كقاعدة بيانات ، فإننا نفقد كل شيء اعتدنا عليه - كسلان
إعلامات التحميل
وتغيير البيانات . لم نكن نريد التخلي عن كل هذه الأشياء ، ولكن في نفس الوقت استخدم مكتبة Paging.
ربما لسنا أول من يحتاج إليها
أسفر البحث السريع عن حل على الفور - مكتبة
مملكة Realmy . بعد دراسة سريعة ، اتضح أن هذا الحل لا يناسبنا - لا تدعم المكتبة التحميل الكسول أو الإخطارات. اضطررت لإنشاء بلدي.
لذلك ، المتطلبات هي:
- الاستمرار في استخدام عالم ؛
- حفظ التحميل كسول للعالم.
- حفظ الإخطارات.
- استخدم مكتبة Paging لتحميل البيانات من قاعدة البيانات وصفحات ترقيم الصفحات من الخادم ، تمامًا كما تقترح مكتبة Paging.
من البداية ، دعونا نحاول معرفة كيفية عمل مكتبة Paging ، وما يجب القيام به لجعلنا نشعر بالرضا.
باختصار - تتكون المكتبة من المكونات التالية:
مصدر البيانات - الفئة الأساسية لتحميل صفحة البيانات حسب الصفحة.
له تطبيقات: PageKeyedDataSource و PositionalDataSource و ItemKeyedDataSource ، لكن الغرض منها ليس مهمًا بالنسبة لنا الآن.
PagedList - قائمة تقوم بتحميل البيانات في مجموعات من DataSource. ولكن بما أننا نستخدم Realm ، فإن تحميل البيانات على دفعات لا يناسبنا.
PagedListAdapter - الفئة المسؤولة عن عرض البيانات التي تم تحميلها بواسطة PagedList.
في الكود المصدري للتطبيق المرجعي ، سنرى كيف تعمل الدائرة.
1. يستدعي PagedListAdapter في أسلوب getItem (مؤشر int) loadAround (int index) لطريقة PagedList:
@SuppressWarnings("WeakerAccess") @Nullable public T getItem(int index) { if (mPagedList == null) { if (mSnapshot == null) { throw new IndexOutOfBoundsException( "Item count is zero, getItem() call is invalid"); } else { return mSnapshot.get(index); } } mPagedList.loadAround(index); return mPagedList.get(index); }
2. PagedList ينفذ الشيكات ويستدعي طريقة الفراغ tryDispatchBoundaryCallbacks (منشور منطقي):
public void loadAround(int index) { if (index < 0 || index >= size()) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size()); } mLastLoad = index + getPositionOffset(); loadAroundInternal(index); mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index); mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index); tryDispatchBoundaryCallbacks(true); }
3. في هذه الطريقة ، يتم التحقق من الحاجة إلى تنزيل الجزء التالي من البيانات ويحدث طلب التنزيل:
@SuppressWarnings("WeakerAccess") void tryDispatchBoundaryCallbacks(boolean post) { final boolean dispatchBegin = mBoundaryCallbackBeginDeferred && mLowestIndexAccessed <= mConfig.prefetchDistance; final boolean dispatchEnd = mBoundaryCallbackEndDeferred && mHighestIndexAccessed >= size() - 1 - mConfig.prefetchDistance; if (!dispatchBegin && !dispatchEnd) { return; } if (dispatchBegin) { mBoundaryCallbackBeginDeferred = false; } if (dispatchEnd) { mBoundaryCallbackEndDeferred = false; } if (post) { mMainThreadExecutor.execute(new Runnable() { @Override public void run() { dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); } }); } else { dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); } }
4. ونتيجة لذلك ، تقع جميع المكالمات في مصدر البيانات ، حيث يتم تنزيل البيانات من قاعدة البيانات أو من مصادر أخرى:
@SuppressWarnings("WeakerAccess") void dispatchBoundaryCallbacks(boolean begin, boolean end) {
في حين أن كل شيء يبدو بسيطًا - فما عليك سوى القيام به. العمل فقط:
- قم بإنشاء تطبيقك الخاص لـ PagedList (RealmPagedList) والذي سيعمل مع RealmModel ؛
- قم بإنشاء تطبيقك الخاص لـ PagedStorage (RealmPagedStorage) ، والذي سيعمل مع OrderedRealmCollection ؛
- قم بإنشاء تطبيق DataSource الخاص بك (RealmDataSource) والذي سيعمل مع RealmModel ؛
- قم بإنشاء محول خاص بك للعمل مع RealmList ؛
- إزالة لا لزوم لها ، إضافة ما يلزم ؛
- القيام به.
نحذف التفاصيل الفنية البسيطة ، وهنا تكون النتيجة -
مكتبة RealmPagination . دعنا نحاول إنشاء تطبيق يعرض قائمة المستخدمين.
0. أضف المكتبة إلى المشروع:
allprojects { repositories { maven { url "https://jitpack.io" } } } implementation 'com.github.magora-android:realmpagination:1.0.0'
1. قم بإنشاء فئة المستخدم:
@Serializable @RealmClass open class User : RealmModel { @PrimaryKey @SerialName("id") var id: Int = 0 @SerialName("login") var login: String? = null @SerialName("avatar_url") var avatarUrl: String? = null @SerialName("url") var url: String? = null @SerialName("html_url") var htmlUrl: String? = null @SerialName("repos_url") var reposUrl: String? = null }
2. إنشاء مصدر بيانات:
class UsersListDataSourceFactory( private val getUsersUseCase: GetUserListUseCase, private val localStorage: UserDataStorage ) : RealmDataSource.Factory<Int, User>() { override fun create(): RealmDataSource<Int, User> { val result = object : RealmPageKeyedDataSource<Int, User>() { override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, User>) {...} override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { ... } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { ... } } return result } override fun destroy() { } }
3. إنشاء محول:
class AdapterUserList( data: RealmPagedList<*, User>, private val onClick: (Int, Int) -> Unit ) : BaseRealmListenableAdapter<User, RecyclerView.ViewHolder>(data) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { ... } }
4. إنشاء ViewModel:
private const val INITIAL_PAGE_SIZE = 50 private const val PAGE_SIZE = 30 private const val PREFETCH_DISTANCE = 10 class VmUsersList( app: Application, private val dsFactory: UsersListDataSourceFactory, ) : AndroidViewModel(app), KoinComponent { val contentData: RealmPagedList<Int, User> get() { val config = RealmPagedList.Config.Builder() .setInitialLoadSizeHint(INITIAL_PAGE_SIZE) .setPageSize(PAGE_SIZE) .setPrefetchDistance(PREFETCH_DISTANCE) .build() return RealmPagedListBuilder(dsFactory, config) .setInitialLoadKey(0) .setRealmData(localStorage.getUsers().users) .build() } fun refreshData() { ... } fun retryAfterPaginationError() { ... } override fun onCleared() { super.onCleared() dsFactory.destroy() } }
5. تهيئة القائمة:
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.adapter = AdapterUserList(viewModel.contentData) { user, position ->
6. إنشاء جزء مع قائمة:
class FragmentUserList : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.adapter = AdapterUserList(viewModel.contentData) { user, position -> ... } }
7. تم
اتضح أن استخدام عالم بسيط مثل الغرفة. نشر سيرجي
شفرة المصدر للمكتبة ومثال للاستخدام . لن تضطر إلى قطع دراجة أخرى إذا واجهت وضعا مماثلا.