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:
- Continúa usando Realm;
- Guardar carga lenta para Realm;
- Guardar notificaciones;
- 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:
@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):
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. En este método, se verifica la necesidad de descargar la siguiente porción de datos y se produce una solicitud de descarga:
@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. Como resultado, todas las llamadas caen en DataSource, donde los datos se descargan de la base de datos o de otras fuentes:
@SuppressWarnings("WeakerAccess") void dispatchBoundaryCallbacks(boolean begin, boolean end) {
Si bien todo parece simple, simplemente tómalo y hazlo. Solo negocios:
- Cree su propia implementación de PagedList (RealmPagedList) que funcionará con RealmModel;
- Cree su propia implementación de PagedStorage (RealmPagedStorage), que funcionará con OrderedRealmCollection;
- Cree su propia implementación de DataSource (RealmDataSource) que funcionará con RealmModel;
- Cree su propio adaptador para trabajar con RealmList;
- Elimine innecesariamente, agregue lo necesario;
- 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.