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:
- Continue usando o Domínio;
- Economize carregamento lento no Realm;
- Salvar notificações;
- 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:
@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):
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. Nesse método, a necessidade de baixar a seguinte parte dos dados é verificada e ocorre uma solicitação de download:
@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 as chamadas caem no DataSource, onde os dados são baixados do banco de dados ou de outras fontes:
@SuppressWarnings("WeakerAccess") void dispatchBoundaryCallbacks(boolean begin, boolean end) {
Enquanto tudo parece simples - basta pegar e fazer. Apenas negócios:
- Crie sua própria implementação do PagedList (RealmPagedList), que funcionará com o RealmModel;
- Crie sua própria implementação de PagedStorage (RealmPagedStorage), que funcionará com OrderedRealmCollection;
- Crie sua própria implementação do DataSource (RealmDataSource), que funcionará com o RealmModel;
- Crie seu próprio adaptador para trabalhar com o RealmList;
- Remova desnecessário, adicione o necessário;
- 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.