Verwenden der Paging-Bibliothek mit Realm

Bei einem der Meetings der Android-Abteilung habe ich mitbekommen, wie einer unserer Entwickler eine kleine Bibliothek erstellt hat, mit deren Hilfe bei der Verwendung von Realm eine „endlose“ Liste erstellt werden kann, wobei das „verzögerte Laden“ und die Benachrichtigungen erhalten bleiben.

Ich habe einen Artikelentwurf gemacht und geschrieben, der fast unverändert ist und den ich mit Ihnen teile. Er seinerseits versprach, Aufgaben zu übernehmen und Kommentare abzugeben, wenn Fragen auftauchen.

Endlose Listen- und schlüsselfertige Lösungen


Eine der Aufgaben, mit denen wir konfrontiert sind, besteht darin, Informationen in einer Liste anzuzeigen, durch die beim Scrollen die Daten geladen und unsichtbar für den Benutzer eingefügt werden. Für den Benutzer sieht es so aus, als würde er eine endlose Liste scrollen.

Der Algorithmus ist ungefähr der folgende:

  • Wir erhalten Daten aus dem Cache für die erste Seite.
  • Wenn der Cache leer ist, erhalten wir die Serverdaten, zeigen sie in der Liste an und schreiben in die Datenbank.
  • Wenn ein Cache vorhanden ist, laden Sie ihn in die Liste.
  • Wenn wir das Ende der Datenbank erreichen, fordern wir Daten vom Server an, zeigen sie in der Liste an und schreiben in die Datenbank.

Vereinfacht: Um die Liste anzuzeigen, wird zuerst der Cache abgefragt, und das Signal zum Laden neuer Daten ist das Ende des Caches.

Um endloses Scrollen zu implementieren, können Sie vorgefertigte Lösungen verwenden:


Wir verwenden Realm als mobile Datenbank. Nachdem wir alle oben genannten Ansätze ausprobiert haben, haben wir die Verwendung der Paging-Bibliothek eingestellt.

Auf den ersten Blick ist die Android Paging Library eine hervorragende Lösung zum Herunterladen von Daten. Wenn Sie SQLite in Verbindung mit Room verwenden, eignet es sich hervorragend als Datenbank. Wenn wir jedoch Realm als Datenbank verwenden, verlieren wir alles, was wir so gewohnt sind - verzögertes Laden und Benachrichtigungen über Datenänderungen . Wir wollten nicht auf all diese Dinge verzichten, sondern gleichzeitig die Paging-Bibliothek nutzen.

Vielleicht sind wir nicht die Ersten, die es brauchen


Eine schnelle Suche ergab sofort eine Lösung - die Realm Monarchy Library. Nach einer kurzen Studie stellte sich heraus, dass diese Lösung nicht zu uns passt - die Bibliothek unterstützt weder verzögertes Laden noch Benachrichtigungen. Ich musste meine eigenen erstellen.

Die Anforderungen sind also:

  1. Verwenden Sie Realm weiterhin.
  2. Speichern Sie das verzögerte Laden für Realm.
  3. Benachrichtigungen speichern;
  4. Verwenden Sie die Paging-Bibliothek, um Daten aus der Datenbank zu laden und Daten vom Server zu paginieren, wie es die Paging-Bibliothek vorschlägt.

Versuchen wir von Anfang an herauszufinden, wie die Paging-Bibliothek funktioniert und was zu tun ist, damit wir uns gut fühlen.

Kurz gesagt - die Bibliothek besteht aus folgenden Komponenten:

DataSource - die Basisklasse zum seitlichen Laden von Daten.
Es hat Implementierungen: PageKeyedDataSource, PositionalDataSource und ItemKeyedDataSource, aber ihr Zweck ist für uns jetzt nicht wichtig.

PagedList - Eine Liste, die Daten in Blöcken aus einer DataSource lädt. Da wir jedoch Realm verwenden, ist das Laden von Daten in Stapeln für uns nicht relevant.
PagedListAdapter - Die Klasse, die für die Anzeige der von der PagedList geladenen Daten verantwortlich ist.

Im Quellcode der Referenzimplementierung werden wir sehen, wie die Schaltung funktioniert.

1. Der PagedListAdapter in der Methode getItem (int index) ruft die Methode loadAround (int index) für die PagedList auf:

/** * Get the item from the current PagedList at the specified index. * <p> * Note that this operates on both loaded items and null padding within the PagedList. * * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}. * @return The item, or null, if a null placeholder is at the specified position. */ @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 führt Überprüfungen durch und ruft die void-Methode tryDispatchBoundaryCallbacks (boolescher Beitrag) auf:

 /** * Load adjacent items to passed index. * * @param index Index at which to load. */ 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); /* * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded, * and accesses happen near the boundaries. * * Note: we post here, since RecyclerView may want to add items in response, and this * call occurs in PagedListAdapter bind. */ tryDispatchBoundaryCallbacks(true); } 

3. Bei dieser Methode wird überprüft, ob der folgende Teil der Daten heruntergeladen werden muss, und es erfolgt eine Downloadanforderung:

 /** * Call this when mLowest/HighestIndexAccessed are changed, or * mBoundaryCallbackBegin/EndDeferred is set. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ 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. Infolgedessen fallen alle Aufrufe in die DataSource, wo die Daten aus der Datenbank oder aus anderen Quellen heruntergeladen werden:

 @SuppressWarnings("WeakerAccess") /* synthetic access */ void dispatchBoundaryCallbacks(boolean begin, boolean end) { // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present if (begin) { //noinspection ConstantConditions mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem()); } if (end) { //noinspection ConstantConditions mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem()); } } 

Während alles einfach aussieht - nimm es einfach und mach es. Nur Geschäft:

  1. Erstellen Sie Ihre eigene Implementierung von PagedList (RealmPagedList), die mit RealmModel funktioniert.
  2. Erstellen Sie Ihre eigene Implementierung von PagedStorage (RealmPagedStorage), die mit OrderedRealmCollection funktioniert.
  3. Erstellen Sie Ihre eigene Implementierung von DataSource (RealmDataSource), die mit RealmModel funktioniert.
  4. Erstellen Sie Ihren eigenen Adapter für die Arbeit mit RealmList.
  5. Entfernen Sie unnötige, fügen Sie die notwendigen hinzu;
  6. Fertig.

Wir lassen die kleinen technischen Details weg und hier ist das Ergebnis - RealmPagination-Bibliothek . Versuchen wir, eine Anwendung zu erstellen, die eine Liste der Benutzer anzeigt.

0. Fügen Sie die Bibliothek zum Projekt hinzu:

 allprojects { repositories { maven { url "https://jitpack.io" } } } implementation 'com.github.magora-android:realmpagination:1.0.0' 


1. Erstellen Sie die Benutzerklasse:

 @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. Erstellen Sie eine DataSource:

 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. Erstellen Sie einen Adapter:

 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. Erstellen Sie ein 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. Initialisieren Sie die Liste:

 recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.adapter = AdapterUserList(viewModel.contentData) { user, position -> //... } 

6. Erstellen Sie ein Fragment mit einer Liste:

 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. Fertig.

Es stellte sich heraus, dass die Verwendung von Realm so einfach ist wie Room. Sergey veröffentlichte den Quellcode der Bibliothek und ein Anwendungsbeispiel . Sie müssen kein anderes Fahrrad schneiden, wenn Sie in eine ähnliche Situation geraten.

Source: https://habr.com/ru/post/de467097/


All Articles