Utilisation de la bibliothèque de pagination avec Realm

Lors d'une des réunions du département Android, j'ai entendu comment un de nos développeurs a créé une petite bibliothèque qui aide à faire une liste «sans fin» lors de l'utilisation de Realm, en préservant le «chargement paresseux» et les notifications.

J'ai rédigé et rédigé un projet d'article, qui est presque inchangé, je le partage avec vous. Pour sa part, il a promis de ratisser les tâches et de faire des commentaires si des questions se posaient.

Liste infinie et solutions clé en main


L'une des tâches auxquelles nous sommes confrontés consiste à afficher des informations dans une liste, lorsque vous la parcourez, les données sont chargées et insérées de manière invisible pour l'utilisateur. Pour l'utilisateur, il semble qu'il fera défiler une liste sans fin.

L'algorithme est approximativement le suivant:

  • nous obtenons des données du cache pour la première page;
  • si le cache est vide - nous obtenons les données du serveur, les affichons dans la liste et écrivons dans la base de données;
  • s'il y a un cache, chargez-le dans la liste;
  • si nous atteignons la fin de la base de données, nous demandons des données au serveur, les affichons dans la liste et écrivons dans la base de données.

Simplifié: pour afficher la liste, le cache est d'abord interrogé et le signal de chargement de nouvelles données est la fin du cache.

Pour implémenter un défilement sans fin, vous pouvez utiliser des solutions toutes faites:


Nous utilisons Realm comme base de données mobile et après avoir essayé toutes les approches ci-dessus, nous nous sommes arrêtés à l'utilisation de la bibliothèque de pagination.

À première vue, la bibliothèque de pagination Android est une excellente solution pour télécharger des données, et lors de l'utilisation de sqlite en conjonction avec Room, elle est excellente comme base de données. Cependant, lorsque vous utilisez Realm comme base de données, nous perdons tout ce à quoi nous sommes habitués: notifications de chargement paresseux et de modification des données . Nous ne voulions pas renoncer à toutes ces choses, mais en même temps utiliser la bibliothèque de pagination.

Peut-être que nous ne sommes pas les premiers à en avoir besoin


Une recherche rapide a immédiatement donné une solution - la bibliothèque de la monarchie du royaume . Après une étude rapide, il s'est avéré que cette solution ne nous convenait pas - la bibliothèque ne prend en charge ni le chargement paresseux ni les notifications. J'ai dû créer le mien.

Ainsi, les exigences sont les suivantes:

  1. Continuez à utiliser Realm;
  2. Enregistrer le chargement paresseux pour le domaine;
  3. Enregistrer les notifications;
  4. Utilisez la bibliothèque de pagination pour charger les données de la base de données et paginer les données du serveur, comme le suggère la bibliothèque de pagination.

Dès le début, essayons de comprendre comment fonctionne la bibliothèque de pagination et ce qu'il faut faire pour que nous nous sentions bien.

En bref - la bibliothèque comprend les composants suivants:

DataSource - la classe de base pour le chargement des données page par page.
Il a des implémentations: PageKeyedDataSource, PositionalDataSource et ItemKeyedDataSource, mais leur objectif n'est plus important pour nous maintenant.

PagedList - une liste qui charge des données en morceaux à partir d'un DataSource. Mais puisque nous utilisons Realm, le chargement de données par lots n'est pas pertinent pour nous.
PagedListAdapter - la classe responsable de l'affichage des données chargées par PagedList.

Dans le code source de l'implémentation de référence, nous verrons comment fonctionne le circuit.

1. Le PagedListAdapter dans la méthode getItem (int index) appelle la méthode loadAround (int index) pour le PagedList:

/** * 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 effectue des vérifications et appelle la méthode void tryDispatchBoundaryCallbacks (publication booléenne):

 /** * 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. Dans cette méthode, la nécessité de télécharger la partie de données suivante est vérifiée et une demande de téléchargement se produit:

 /** * 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. Par conséquent, tous les appels tombent dans le DataSource, où les données sont téléchargées à partir de la base de données ou d'autres sources:

 @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()); } } 

Alors que tout semble simple - prenez-le et faites-le. Juste affaires:

  1. Créez votre propre implémentation de PagedList (RealmPagedList) qui fonctionnera avec RealmModel;
  2. Créez votre propre implémentation de PagedStorage (RealmPagedStorage), qui fonctionnera avec OrderedRealmCollection;
  3. Créez votre propre implémentation de DataSource (RealmDataSource) qui fonctionnera avec RealmModel;
  4. Créez votre propre adaptateur pour travailler avec RealmList;
  5. Retirez inutile, ajoutez le nécessaire;
  6. C'est fait.

Nous omettons les détails techniques mineurs, et voici le résultat - la bibliothèque RealmPagination . Essayons de créer une application qui affiche une liste d'utilisateurs.

0. Ajoutez la bibliothèque au projet:

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


1. Créez la classe Utilisateur:

 @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. Créez une source de données:

 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. Créez un adaptateur:

 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. Créez un 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. Initialisez la liste:

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

6. Créez un fragment avec une 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. Terminé.

Il s'est avéré que l'utilisation de Realm est aussi simple que Room. Sergey a publié le code source de la bibliothèque et un exemple d'utilisation . Vous n'aurez pas à couper un autre vélo si vous rencontrez une situation similaire.

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


All Articles