Usando a biblioteca de paginação com o Realm

Em uma das reuniões do departamento do Android, ouvi como um de nossos desenvolvedores criou uma pequena biblioteca que ajuda a criar uma lista "interminável" ao usar o Realm, preservando o "carregamento lento" e as notificações.

Eu criei e escrevi um artigo preliminar, quase inalterado, que estou compartilhando com você. Por sua parte, ele prometeu que faria tarefas e entraria em comentários se surgissem dúvidas.

Soluções intermináveis ​​de lista e chave na mão


Uma das tarefas que enfrentamos é exibir informações em uma lista, ao rolar pela qual os dados são carregados e inseridos invisivelmente para o usuário. Para o usuário, parece que ele irá rolar uma lista sem fim.

O algoritmo é aproximadamente o seguinte:

  • obtemos dados do cache para a primeira página;
  • se o cache estiver vazio - obtemos os dados do servidor, exibimos na lista e escrevemos no banco de dados;
  • se houver um cache, carregue-o na lista;
  • se chegarmos ao final do banco de dados, solicitamos dados do servidor, os exibimos na lista e escrevemos no banco de dados.

Simplificado: para exibir a lista, o cache é pesquisado primeiro e o sinal para carregar novos dados é o fim do cache.

Para implementar a rolagem sem fim, você pode usar soluções prontas:


Usamos o Realm como um banco de dados móvel e, tendo tentado todas as abordagens acima, paramos de usar a biblioteca de paginação.

À primeira vista, a Android Paging Library é uma excelente solução para baixar dados e, ao usar o sqlite em conjunto com o Room, é excelente como banco de dados. No entanto, ao usar o Realm como banco de dados, perdemos tudo o que estamos acostumados - notificações de carregamento lento e alteração de dados . Não queríamos desistir de todas essas coisas, mas ao mesmo tempo, usamos a biblioteca de paginação.

Talvez não sejamos os primeiros a precisar


Uma pesquisa rápida produziu imediatamente uma solução - a biblioteca de monarquia do reino . Após um rápido estudo, verificou-se que essa solução não nos convém - a biblioteca não suporta carregamento lento ou notificações. Eu tive que criar o meu.

Portanto, os requisitos são:

  1. Continue usando o Domínio;
  2. Economize carregamento lento no Realm;
  3. Salvar notificações;
  4. Use a biblioteca de paginação para carregar dados do banco de dados e paginar dados do servidor, exatamente como sugere a biblioteca de paginação.

Desde o início, vamos tentar descobrir como a biblioteca de Paginação funciona e o que fazer para nos sentirmos bem.

Resumidamente - a biblioteca consiste nos seguintes componentes:

DataSource - a classe base para carregar dados página por página.
Possui implementações: PageKeyedDataSource, PositionalDataSource e ItemKeyedDataSource, mas seu objetivo não é importante para nós agora.

PagedList - uma lista que carrega dados em pedaços de um DataSource. Porém, como usamos o Domínio, o carregamento de dados em lotes não é relevante para nós.
PagedListAdapter - a classe responsável por exibir os dados carregados pelo PagedList.

No código fonte da implementação de referência, veremos como o circuito funciona.

1. O PagedListAdapter no método getItem (int index) chama o método loadAround (int index) para o 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 executa verificações e chama o método void tryDispatchBoundaryCallbacks (postagem 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. Nesse método, a necessidade de baixar a seguinte parte dos dados é verificada e ocorre uma solicitação de download:

 /** * 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 as chamadas caem no DataSource, onde os dados são baixados do banco de dados ou de outras fontes:

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

Enquanto tudo parece simples - basta pegar e fazer. Apenas negócios:

  1. Crie sua própria implementação do PagedList (RealmPagedList), que funcionará com o RealmModel;
  2. Crie sua própria implementação de PagedStorage (RealmPagedStorage), que funcionará com OrderedRealmCollection;
  3. Crie sua própria implementação do DataSource (RealmDataSource), que funcionará com o RealmModel;
  4. Crie seu próprio adaptador para trabalhar com o RealmList;
  5. Remova desnecessário, adicione o necessário;
  6. Feito.

Nós omitimos os pequenos detalhes técnicos, e aqui está o resultado - a biblioteca RealmPagination . Vamos tentar criar um aplicativo que exibe uma lista de usuários.

0. Adicione a biblioteca ao projeto:

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


1. Crie a classe de usuário:

 @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. Crie uma fonte de dados:

 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. Crie um 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. Crie um 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. Inicialize a lista:

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

6. Crie um fragmento com uma 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. Feito.

Descobriu-se que o uso do Realm é tão simples quanto o Room. Sergey postou o código fonte da biblioteca e um exemplo de uso . Você não precisará cortar outra bicicleta se tiver uma situação semelhante.

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


All Articles