Uso de la biblioteca de paginación con Realm

En una de las reuniones del departamento de Android, escuché cómo uno de nuestros desarrolladores creó una pequeña biblioteca que ayuda a hacer una lista "interminable" al usar Realm, preservando la "carga diferida" y las notificaciones.

Hice y escribí un borrador del artículo, que casi no ha cambiado, lo comparto con ustedes. Por su parte, prometió que realizaría tareas y presentaría comentarios si surgieran preguntas.

Lista interminable y soluciones llave en mano


Una de las tareas a las que nos enfrentamos es mostrar información en una lista, cuando se desplaza por ella, los datos se cargan y se insertan de forma invisible para el usuario. Para el usuario, parece que desplazará una lista interminable.

El algoritmo es aproximadamente el siguiente:

  • obtenemos datos del caché para la primera página;
  • si el caché está vacío, obtenemos los datos del servidor, los mostramos en la lista y escribimos en la base de datos;
  • si hay un caché, cárguelo en la lista;
  • Si llegamos al final de la base de datos, solicitamos datos del servidor, los mostramos en la lista y escribimos en la base de datos.

Simplificado: para mostrar la lista, primero se sondea el caché y la señal para cargar nuevos datos es el final del caché.

Para implementar el desplazamiento sin fin, puede usar soluciones preparadas:


Usamos Realm como una base de datos móvil, y después de haber probado todos los enfoques anteriores, nos detuvimos para usar la biblioteca Paging.

A primera vista, Android Paging Library es una excelente solución para descargar datos, y cuando se utiliza sqlite junto con Room, es excelente como base de datos. Sin embargo, cuando usamos Realm como base de datos, perdemos todo a lo que estamos acostumbrados: carga lenta y notificaciones de cambio de datos . No queríamos renunciar a todas estas cosas, pero al mismo tiempo usar la biblioteca Paging.

Quizás no somos los primeros en necesitarlo


Una búsqueda rápida arrojó inmediatamente una solución: la biblioteca de la monarquía del Reino . Después de un rápido estudio, resultó que esta solución no nos conviene: la biblioteca no admite la carga diferida ni las notificaciones. Tuve que crear el mío.

Entonces, los requisitos son:

  1. Continúa usando Realm;
  2. Guardar carga lenta para Realm;
  3. Guardar notificaciones;
  4. Use la biblioteca de paginación para cargar datos de la base de datos y paginar datos del servidor, tal como sugiere la biblioteca de paginación.

Desde el principio, intentemos descubrir cómo funciona la biblioteca de Paginación y qué hacer para que nos sintamos bien.

Brevemente: la biblioteca consta de los siguientes componentes:

DataSource : la clase base para cargar datos página por página.
Tiene implementaciones: PageKeyedDataSource, PositionalDataSource y ItemKeyedDataSource, pero su propósito no es importante para nosotros ahora.

PagedList : una lista que carga datos en fragmentos de un DataSource. Pero como usamos Realm, cargar datos en lotes no es relevante para nosotros.
PagedListAdapter: la clase responsable de mostrar los datos cargados por PagedList.

En el código fuente de la implementación de referencia, veremos cómo funciona el circuito.

1. El PagedListAdapter en el método getItem (int index) llama al método loadAround (int index) para 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 realiza comprobaciones y llama al método vacío tryDispatchBoundaryCallbacks (publicación booleana):

 /** * 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. En este método, se verifica la necesidad de descargar la siguiente porción de datos y se produce una solicitud de descarga:

 /** * 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. Como resultado, todas las llamadas caen en DataSource, donde los datos se descargan de la base de datos o de otras fuentes:

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

Si bien todo parece simple, simplemente tómalo y hazlo. Solo negocios:

  1. Cree su propia implementación de PagedList (RealmPagedList) que funcionará con RealmModel;
  2. Cree su propia implementación de PagedStorage (RealmPagedStorage), que funcionará con OrderedRealmCollection;
  3. Cree su propia implementación de DataSource (RealmDataSource) que funcionará con RealmModel;
  4. Cree su propio adaptador para trabajar con RealmList;
  5. Elimine innecesariamente, agregue lo necesario;
  6. Listo

Omitimos los detalles técnicos menores, y aquí está el resultado: la biblioteca RealmPagination . Intentemos crear una aplicación que muestre una lista de usuarios.

0. Agregue la biblioteca al proyecto:

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


1. Cree la clase de usuario:

 @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. Crear una fuente de datos:

 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. Crear un adaptador:

 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. Crear un modelo de vista:

 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. Inicialice la lista:

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

6. Cree un fragmento con una lista:

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

Resultó que usar Realm es tan simple como Room. Sergey publicó el código fuente de la biblioteca y un ejemplo de uso . No tendrá que cortar otra bicicleta si se encuentra con una situación similar.

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


All Articles