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:
- Verwenden Sie Realm weiterhin.
- Speichern Sie das verzögerte Laden für Realm.
- Benachrichtigungen speichern;
- 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:
@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:
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. Bei dieser Methode wird überprüft, ob der folgende Teil der Daten heruntergeladen werden muss, und es erfolgt eine Downloadanforderung:
@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. Infolgedessen fallen alle Aufrufe in die DataSource, wo die Daten aus der Datenbank oder aus anderen Quellen heruntergeladen werden:
@SuppressWarnings("WeakerAccess") void dispatchBoundaryCallbacks(boolean begin, boolean end) {
Während alles einfach aussieht - nimm es einfach und mach es. Nur Geschäft:
- Erstellen Sie Ihre eigene Implementierung von PagedList (RealmPagedList), die mit RealmModel funktioniert.
- Erstellen Sie Ihre eigene Implementierung von PagedStorage (RealmPagedStorage), die mit OrderedRealmCollection funktioniert.
- Erstellen Sie Ihre eigene Implementierung von DataSource (RealmDataSource), die mit RealmModel funktioniert.
- Erstellen Sie Ihren eigenen Adapter für die Arbeit mit RealmList.
- Entfernen Sie unnötige, fügen Sie die notwendigen hinzu;
- 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.