MVVM e seleção de elementos no adaptador

Já depois do terceiro adaptador samopisny em que era necessário implementar a lógica de memorização do elemento selecionado, pensei que deveria haver alguma solução que já incluísse tudo o necessário. Especialmente se durante o processo de desenvolvimento você tiver que alterar a capacidade de selecionar apenas um item para múltipla escolha.


Depois de estudar a abordagem MVVM e mergulhar completamente nela, a questão acima mencionada surgiu muito mais acentuadamente. Além disso, o próprio adaptador está no nível View , enquanto as informações sobre os elementos selecionados geralmente são extremamente necessárias para o ViewModel .


Talvez eu não tenha gasto tempo suficiente procurando respostas na Internet, mas, em qualquer caso, não encontrei uma solução pronta. No entanto, em um dos projetos, tive uma idéia de implementação que poderia muito bem ser universal, então queria compartilhá-la.


Observação . Embora seja lógico e apropriado que o MVVM no Android faça uma implementação com o LiveData , neste estágio não estou pronto para escrever código usando-o. Então, isso é apenas para o futuro. Mas a solução final acabou sem dependências do Android , o que potencialmente torna possível usá-lo em qualquer plataforma em que o kotlin possa funcionar.


SelectionManager


Para resolver esse problema, a interface geral do SelectionManager foi compilada:


 interface SelectionManager { fun clearSelection() fun selectPosition(position: Int) fun isPositionSelected(position: Int): Boolean fun registerSelectionChangeListener(listener: (position: Int, isSelected: Boolean) -> Unit): Disposable fun getSelectedPositions(): ArrayList<Int> fun isAnySelected(): Boolean fun addSelectionInterceptor(interceptor: (position: Int, isSelected: Boolean, callback: () -> Unit) -> Unit): Disposable } 

Por padrão, já existem 3 implementações diferentes:


  • MultipleSelection - o objeto permite selecionar quantos elementos você quiser da lista;
  • Seleção Única - um objeto permite selecionar apenas um elemento;
  • NoneSelection - o objeto não permite selecionar elementos.

Provavelmente, com o último, haverá mais de todas as perguntas, então tentarei mostrar um exemplo.


Adaptador


Ele deve adicionar o objeto SelectionManager ao adaptador como uma dependência por meio do construtor.


 class TestAdapter(private val selectionManager: SelectionManager) : RecyclerView.Adapter<TestHolder>() { //class body } 

No exemplo, eu não vou me preocupar com a lógica de processar o clique em um elemento, por isso concordamos que o titular (sem detalhes) é totalmente responsável pela nomeação do ouvinte de clique.


 class TestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: Any, onItemClick: () -> Unit) { //all bind logic } } 

Além disso, para que essa mágica funcione, o adaptador deve executar as 3 etapas a seguir:


1. onBindViewHolder


Passe um retorno de chamada para o método de bind do titular, que chamará selectionManager.selectPosition(position) para o elemento exibido. Também aqui, você provavelmente precisará alterar a exibição (geralmente apenas o plano de fundo), dependendo de o item estar atualmente selecionado - você pode chamar selectionManager.isPositionSelected(position) para isso.


 override fun onBindViewHolder(holder: TestHolder, position: Int) { val isItemSelected = selectionManager.isPositionSelected(position) //do whatever you need depending on `isItemSelected` value val item = ... //get current item by `position` value holder.bind(item) { selectionManager.selectPosition(position) } } 

2. registerSelectionChangeListener


Para que o adaptador atualize os elementos pressionados em tempo hábil, você deve assinar a ação apropriada. E não esqueça que o resultado retornado pelo método de assinatura deve ser salvo.


 private val selectionDisposable = selectionManager.registerSelectionChangeListener { position, isSelected -> notifyItemChanged(position) } 

Observo que, neste caso, o valor do parâmetro isSelected não isSelected importante, pois com qualquer alteração a aparência do elemento é alterada. Mas nada impede que você adicione processamento adicional, para o qual esse valor é importante.


3. selectionDisposable


Na etapa anterior, não disse apenas que o resultado do método deve ser salvo - é retornado um objeto que limpa a assinatura para evitar vazamentos. Após o término do trabalho, esse objeto deve ser consultado.


 fun destroy() { selectionDisposable.dispose() } 

ViewModel


Para que o adaptador mágico seja suficiente, passaremos para o ViewModel . A inicialização do SelectionManager extremamente simples:


 class TestViewModel: ViewModel() { val selectionManager: SelectionManager = SingleSelection() } 

Aqui, por analogia com o adaptador, você pode assinar as alterações (por exemplo, para tornar o botão "Excluir" inacessível quando nenhum item for selecionado), mas também pode clicar em um botão de resumo (por exemplo, "Download selecionado") para obter uma lista de todos os itens selecionados. .


 fun onDownloadClick() { val selectedPositions: ArrayList<Int> = selectionManager.getSelectedPositions() ... } 

E aqui um dos pontos negativos da minha solução vem à tona: no estágio atual, o objeto é capaz de armazenar apenas as posições dos elementos. Ou seja, para obter exatamente os objetos selecionados, e não suas posições, será necessária lógica adicional usando a fonte de dados conectada ao adaptador (infelizmente, apenas até o momento). Mas espero que você possa lidar com isso.


Além disso, resta apenas conectar o adaptador ao modelo de visualização. Isso já está no nível da atividade.


 class TestActivity: AppCompatActivity() { private lateinit var adapter: TestAdapter private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //`TestViewModel` initialization adapter = TestAdapter(viewModel.selectionManager) } } 

Flexibilidade


Para alguns, isso pode ser bastante compreensível, mas quero observar adicionalmente que, com essa implementação, é fácil controlar o método de seleção nos adaptadores. Agora o adaptador pode selecionar apenas um elemento, mas se TestViewModel alterar a inicialização da propriedade selectionManager no TestViewModel , o restante do código funcionará "de uma nova maneira" sem exigir nenhuma alteração. Ou seja, defina val selectionManager: SelectionManager = MultipleSelection() e agora o adaptador permite selecionar quantos elementos você desejar.


E se você tiver algum tipo de classe de adaptador base para todo o aplicativo, não poderá ter medo de incluir o SelectionManager da mesma maneira. De fato, especialmente para adaptadores que não implicam a escolha de elementos, existe uma implementação de NoneSelection - não importa o que você faça com ele, ele nunca terá os elementos selecionados e nunca chamará nenhum dos ouvintes. Não, ele não lança exceções - ele simplesmente ignora todas as chamadas, mas o adaptador não precisa saber disso.


Interceptor


Também existem casos em que uma alteração na seleção de um elemento é acompanhada por operações adicionais (por exemplo, carregando informações detalhadas), antes da conclusão bem-sucedida da qual a aplicação das alterações leva a um estado incorreto. Especialmente para esses casos, adicionei um mecanismo de interceptação.


Para adicionar um interceptador, você precisa chamar o método addSelectionInterceptor (novamente, você precisa salvar o resultado e acessá-lo após a conclusão). Um dos parâmetros do interceptor no exemplo de callback: () -> Unit - até que seja chamado, as alterações não serão aplicadas. Ou seja, na ausência de uma rede, o carregamento de informações detalhadas do servidor não pode ser concluído com êxito; portanto, o estado do selectionManager usado não será alterado. Se esse é exatamente o seu comportamento, você precisa deste método.


 val interceptionDisposable = selectionManager.addSelectionInterceptor { position: Int, isSelected: Boolean, callback: () -> Unit -> if(isSelected) { val selectedItem = ... //get current item by `position` value val isDataLoadingSuccessful: Boolean = ... //download data for `selectedItem` if(isDataLoadingSuccessful) { callback() } } } 

Se necessário, você pode conectar quantos interceptores desejar. Nesse caso, a callback() do primeiro interceptador começa a processar o segundo. E apenas callback() no último deles finalmente causará uma alteração no estado selectionManager .


Perspectivas


  1. Usar Disposable para limpar assinaturas é eficaz, mas não tão conveniente quanto o LiveData . A primeira melhoria na linha é usar os recursos do android.arch.lifecycle para um trabalho mais conveniente. Provavelmente, este será um projeto separado, para não adicionar dependência de plataforma ao atual.
  2. Como eu disse, obter uma lista de objetos selecionados acabou sendo inconveniente. Também quero tentar implementar um objeto que possa funcionar com um contêiner de dados da mesma maneira. Ao mesmo tempo, poderia ser uma fonte de dados para o adaptador.

Referências


Você pode encontrar os códigos-fonte no link - GitHub
O projeto também está disponível para implementação através do gradle - ru.ircover.selectionmanager:core:1.0.0

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


All Articles