Desenvolvimento Android. Um pouco sobre o trabalho rápido com listas

Olá pessoal! Minhas postagens desejam ajudar com alguns elementos do Android. Se você é um desenvolvedor que ainda não desenvolveu um algoritmo para criar listas, pode ser útil ler este material. Basicamente, gostaria de oferecer soluções prontas para o desenvolvimento, revelando ao longo da história algumas reflexões sobre como cheguei a isso.

Neste artigo:

  • formamos várias classes base e interfaces para trabalhar com RecyclerView e RecyclerView.Adapter
  • conecte uma biblioteca do Android Jetpack (opcional, primeiro sem ela)
  • para um desenvolvimento ainda mais rápido - a opção de modelo no final do artigo;)

Entrada


Bem então! Todo mundo já se esqueceu do ListView e está escrevendo com segurança no RecyclerView ( Rv ). Aqueles momentos em que implementamos o padrão ViewHolder nós mesmos caímos no esquecimento. Rv nos fornece um conjunto de classes prontas para implementar listas e uma seleção bastante grande de LayoutManagers para exibi-las. De fato, olhando para muitas telas, você pode imaginar a maioria delas como uma lista - precisamente devido à capacidade de cada elemento implementar seu próprio ViewHolder . Nos foi contada a história do desenvolvimento com mais detalhes no Google I / O.

Porém, sempre existem alguns “buts!”. As respostas padrão ao Stackoverflow sugerem soluções gerais que levam à copiar e colar, especialmente no local em que o adaptador é implementado.

No momento, Rvtem três anos. Existem muitas informações e muitas bibliotecas com soluções prontas, mas e se você não precisar de todas as funcionalidades, ou se você procurar o código de outra pessoa - e você vê que o Horror Antigo não é o que você gostaria de ver, ou não o que imaginado? Nesses três anos, o Android finalmente aceitou oficialmente o Kotlin = a legibilidade do código melhorou, muitos artigos interessantes foram publicados no Rv que revelam completamente seus recursos.

O objetivo disso é coletar as bases das melhores práticas de sua bicicleta , uma estrutura para trabalhar com listas para novas aplicações. Essa estrutura pode ser complementada com lógica de aplicativo para aplicativo, usando o que você precisa e descartando desnecessários. Acho que essa abordagem é muito melhor do que a biblioteca de outra pessoa. Nas suas aulas, você tem a oportunidade de entender como tudo funciona e de controlar os casos de que você precisa sem estar ligado à decisão de outra pessoa.

Vamos pensar logicamente e desde o início


O que o componente deve fazer será decidido pela interface, não pela classe , mas no final fecharemos a lógica de implementação concreta da classe que implementará e implementará essa interface. Mas, se acontecer que, com a implementação da interface, uma cópia e colar é formada - podemos ocultá-la atrás de uma classe abstrata e depois dela - uma classe que herda do abstrato. Mostrarei minha implementação das interfaces básicas, mas meu objetivo é que o desenvolvedor tente apenas pensar na mesma direção. Mais uma vez - o plano é este: Um conjunto de interfaces -> uma classe abstrata que usa copiar e colar (se necessário) -> e já uma classe específica com um código exclusivo . Você pode implementar interfaces de maneira diferente.

O que um adaptador pode fazer com uma lista? A resposta a esta pergunta é mais fácil de obter quando você olha para um exemplo. Você pode dar uma olhada no RecyclerView.Adapter, você encontrará algumas dicas. Se você pensa um pouco, pode imaginar algo como estes métodos:

IBaseListAdapter
interface IBaseListAdapter<T> { fun add(newItem: T) fun add(newItems: ArrayList<T>?) fun addAtPosition(pos : Int, newItem : T) fun remove(position: Int) fun clearAll() } 

* Percorrendo os projetos, encontrei vários outros métodos que omitirei aqui, por exemplo, getItemByPos (position: Int) ou até subList (startIndex: Int, endIndex: Int). Repito: você mesmo deve observar o que precisa do projeto e incluir funções na interface. Não é difícil quando você sabe que tudo acontece em uma classe. O ascetismo nessa questão permitirá que você se livre de lógicas desnecessárias, o que degrada a legibilidade do código, porque uma implementação específica exige mais linhas.

Preste atenção ao T. genérico Em geral, o adaptador funciona com qualquer objeto da lista (item), portanto, não há esclarecimentos aqui - ainda não escolhemos nossa abordagem. E neste artigo, haverá pelo menos dois deles, a primeira interface será assim:

 interface IBaseListItem { fun getLayoutId(): Int } 

Bem, sim, parece lógico - estamos falando de um item da lista, portanto cada item deve ter algum tipo de layout e você pode consultá-lo usando layoutId. Provavelmente, um desenvolvedor iniciante provavelmente não precisará de nada, a menos que, obviamente, adotemos abordagens mais avançadas . Se você tem experiência suficiente em desenvolvimento, certamente pode fazer um delegado ou um invólucro, mas vale a pena com um projeto pequeno - e ainda menos experiência em desenvolvimento? Todos os meus links em algum lugar do YouTube são muito úteis se você não tiver tempo agora - lembre-se deles e continue a ler, porque a abordagem é mais simples aqui - acho que, com o trabalho padrão com Rv , a julgar pela documentação oficial , o que é oferecido acima não é implícito.

Está na hora de combinar nosso IBaseListAdapter com interfaces, e a seguinte classe será abstrata:

SimpleListAdapter
 abstract class SimpleListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(), IBaseListAdapter<IBaseListItem> { protected val items: ArrayList<IBaseListItem> = ArrayList() override fun getItemCount() = items.size override fun getItemViewType(position: Int) = items[position].layoutId protected fun inflateByViewType(context: Context?, viewType: Int, parent: ViewGroup) = LayoutInflater.from(context).inflate(viewType, parent, false) override fun add(newItem: IBaseListItem) { items.add(newItem) notifyDataSetChanged() } override fun add(newItems: ArrayList<IBaseListItem>?) { for (newItem in newItems ?: return) { items.add(newItem) notifyDataSetChanged() } } override fun addAtPosition(pos: Int, newItem: IBaseListItem) { items.add(pos, newItem) notifyDataSetChanged() } override fun clearAll() { items.clear() notifyDataSetChanged() } override fun remove(position: Int) { items.removeAt(position) notifyDataSetChanged() } } 

* Nota: Preste atenção à função getItemViewType (position: Int) substituída. Precisamos de algum tipo de chave int, pela qual o Rv entenda qual ViewHolder nos convém. O layoutID do valor do item é muito útil para isso, pois O Android sempre torna útil a identificação dos layouts, e todos os valores são maiores que zero - usaremos isso posteriormente para " inflar " o itemView para nossos visualizadores no método inflateByViewType () (próxima linha).

Crie uma lista


Tomemos, por exemplo, a tela de configurações. O Android nos oferece sua própria versão, mas e se o design precisar de algo mais sofisticado? Eu prefiro preencher esta tela como uma lista. Aqui será dado esse caso:



Como vemos dois itens de lista diferentes, o SimpleListAdapter e o Rv são perfeitos aqui!

Vamos começar! Você pode começar com layouts de layout para item'ov:

item_info.xml; item_switch.xml
 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:id="@+id/tv_info_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:textColor="@color/black" android:textSize="20sp" tools:text="Balance" /> <TextView android:id="@+id/tv_info_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" tools:text="1000 $" /> </FrameLayout> <!----> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:id="@+id/tv_switch_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:textColor="@color/black" android:textSize="20sp" tools:text="Send notifications" /> <Switch android:id="@+id/tv_switch_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" tools:checked="true" /> </FrameLayout> 

Em seguida, definimos as próprias classes, dentro das quais queremos passar os valores que interagem com a lista: o primeiro é o cabeçalho e algum valor que vem de fora (teremos um esboço, sobre solicitações outra vez), o segundo é o cabeçalho e a variável booleana pelo qual devemos executar uma ação. Para distinguir entre os elementos do Switch, o id das entidades do servidor é adequado; se eles não estiverem lá, podemos criá-los durante a inicialização.

InfoItem.kt, SwitchItem.kt
 class InfoItem(val title: String, val value: String): IBaseListItem { override val layoutId = R.layout.item_info } class SwitchItem( val id: Int, val title: String, val actionOnReceive: (itemId: Int, userChoice: Boolean) -> Unit ) : IBaseListItem { override val layoutId = R.layout.item_switch } 

Em uma implementação simples, cada elemento também precisará de um ViewHolder:

InfoViewHolder.kt, SwitchViewHolder.kt
 class InfoViewHolder.kt(view: View) : RecyclerView.ViewHolder(view) { val tvTitle = view.tv_info_title val tvValue = view.tv_info_value } class SwitchViewHolder.kt(view: View) : RecyclerView.ViewHolder(view) { val tvTitle = view.tv_switch_title val tvValue = view.tv_switch_value } 

Bem, a parte mais interessante é a implementação concreta do SimpleListAdapter'a:

SettingsListAdapter.kt
 class SettingsListAdapter : SimpleListAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val context = parent.context return when (viewType) { R.layout.item_info -> InfoHolder(inflateByViewType(context, viewType, parent)) R.layout.item_switch -> SwitchHolder(inflateByViewType(context, viewType, parent)) else -> throw IllegalStateException("There is no match with current layoutId") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is InfoHolder -> { val infoItem = items[position] as InfoItem holder.tvTitle.text = infoItem.title holder.tvValue.text = infoItem.value } is SwitchHolder -> { val switchItem = items[position] as SwitchItem holder.tvTitle.text = switchItem.title holder.tvValue.setOnCheckedChangeListener { _, isChecked -> switchItem.actionOnReceive.invoke(switchItem.id, isChecked) } } else -> throw IllegalStateException("There is no match with current holder instance") } } } 

* Nota: Não esqueça que, sob o capô do método inflateByViewType (contexto, viewType, pai): viewType = layoutId.

Todos os componentes estão prontos! Agora, o código de atividade permanece e você pode executar o programa:

activity_settings.xml
 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/rView" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> 

SettingsActivity.kt
 class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) val adapter = SettingsListAdapter() rView.layoutManager = LinearLayoutManager(this) rView.adapter = adapter adapter.add(InfoItem("User Name", "Leo Allford")) adapter.add(InfoItem("Balance", "350 $")) adapter.add(InfoItem("Tariff", "Business")) adapter.add(SwitchItem(1, "Send Notifications") { itemId, userChoice -> onCheck(itemId, userChoice) }) adapter.add(SwitchItem(2, "Send News on Email") { itemId, userChoice -> onCheck(itemId, userChoice) }) } private fun onCheck(itemId: Int, userChoice: Boolean) { when (itemId) { 1 -> Toast.makeText(this, "Notification now set as $userChoice", Toast.LENGTH_SHORT).show() 2 -> Toast.makeText(this, "Send news now set as $userChoice", Toast.LENGTH_SHORT).show() } } } 

Como resultado, ao criar a lista, todo o trabalho se resume ao seguinte:

1. Cálculo do número de layouts diferentes para itens

2. Escolha os nomes deles. Eu uso a regra: Algo Item.kt, item_ algo .xml, Algo ViewHolder.kt

3. Escrevemos um adaptador para essas classes. Em princípio, se você não pretende otimizar, basta um adaptador comum. Mas em grandes projetos, eu ainda faria alguns, nas telas, porque, no primeiro caso, o método onBindViewHolder () cresce inevitavelmente (a legibilidade do código sofre) no seu adaptador (no nosso caso, é SettingsListAdapter ) + o programa terá que ir sempre, para cada item, execute neste método + no método onCreateViewHolder ()

4. Execute o código e divirta-se!

Jetpack


Até aquele momento, usamos a abordagem de ligação de dados padrão do Item.kt ao nosso item_layout.xml . Mas podemos unificar o método onBindViewHolder () , deixá-lo mínimo e transferir a lógica para Item e layout.

Vamos para a página oficial do Android JetPack:



Vamos prestar atenção na primeira guia na seção Arquitetura. A vinculação de dados do Android é um tópico muito extenso, gostaria de falar sobre isso com mais detalhes em outros artigos, mas por enquanto vamos usá-lo apenas como parte do atual - tornaremos nosso Item.kt - variável para item.xml (ou você pode chamá-lo de um modelo de exibição para layout).

No momento da redação deste artigo, o Databinding poderia ser conectado assim:

 android { compileSdkVersion 27 defaultConfig {...} buildTypes {...} dataBinding { enabled = true } dependencies { kapt "com.android.databinding:compiler:3.1.3" //... } } 

Vamos percorrer as classes base novamente. A interface para o item complementa a anterior:

 interface IBaseItemVm: IBaseListItem { val brVariableId: Int } 

Além disso, expandiremos nosso ViewHolder, portanto, somos contatados com o armazenamento de dados. Passaremos o ViewDataBinding para ele, após o qual esqueceremos com segurança a criação de um layout e ligação de dados

 class VmViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) 

A mesma abordagem é usada aqui , mas no Kotlin parece muito mais curta, não é? =)

VmListAdapter
 class VmListAdapter : RecyclerView.Adapter<VmViewHolder>(), IBaseListAdapter<IBaseItemVm> { private var mItems = ArrayList<IBaseItemVm>() override fun getItemCount() = mItems.size override fun getItemViewType(position: Int) = mItems[position].layoutId override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VmViewHolder { val inflater = LayoutInflater.from(parent.context) val viewDataBinding = DataBindingUtil.inflate<ViewDataBinding>(inflater!!, viewType, parent, false) return VmViewHolder(viewDataBinding) } override fun onBindViewHolder(holder: VmViewHolder, position: Int) { holder.binding.setVariable(mItems[position].brVariableId, mItems[position]) holder.binding.executePendingBindings() } override fun add(newItem: IBaseItemVm) { mItems.add(newItem) notifyItemInserted(mItems.lastIndex) } override fun add(newItems: ArrayList<IBaseItemVm>?) { val oldSize = mItems.size mItems.addAll(newItems!!) notifyItemRangeInserted(oldSize, newItems.size) } override fun clearAll() { mItems.clear() notifyDataSetChanged() } override fun getItemId(position: Int): Long { val pos = mItems.size - position return super.getItemId(pos) } override fun addAtPosition(pos: Int, newItem: IBaseItemVm) { mItems.add(pos, newItem) notifyItemInserted(pos) } override fun remove(position: Int) { mItems.removeAt(position) notifyItemRemoved(position) } } 

Preste atenção geralmente aos métodos onCreateViewHolder () , onBindViewHolder () . A ideia é que eles não cresçam mais. No total, você recebe um adaptador para qualquer tela, com qualquer item da lista.

Nossos itens:

InfoItem.kt, SwitchItem.kt
 class InfoItem(val title: String, val value: String) : IBaseItemVm { override val brVariableId = BR.vmInfo override val layoutId = R.layout.item_info } // class SwitchItem( val id: Int, val title: String, private val actionOnReceive: (itemId: Int, userChoice: Boolean) -> Unit ) : IBaseItemVm { override val brVariableId = BR.vmSwitch override val layoutId = R.layout.item_switch val listener = CompoundButton.OnCheckedChangeListener { _, isChecked -> actionOnReceive.invoke(id, isChecked) } } 

Aqui você pode ver para onde foi a lógica do método onBindViewHolder () . A vinculação de dados do Android assumiu a responsabilidade - agora qualquer um de nosso layout é suportado por seu modelo de exibição e processa calmamente toda a lógica de cliques, animações, consultas e outras coisas. O que você propõe. Os adaptadores de ligação farão um bom trabalho - permitindo conectar a visualização com dados de qualquer tipo. Além disso, a comunicação pode ser melhorada graças ao armazenamento de dados bidirecional . Provavelmente, ele piscará em qualquer um dos artigos a seguir; neste exemplo, tudo pode ser simplificado. Um adaptador é suficiente para nós:

 @BindingAdapter("switchListener") fun setSwitchListener(sw: Switch, listener: CompoundButton.OnCheckedChangeListener) { sw.setOnCheckedChangeListener(listener) } 

Depois disso, vinculamos nossos valores de variável ao item dentro do xml:

item_info.xml; item_switch.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="com.lfkekpoint.adapters.adapters.presentation.modules.bindableItemsSettings.InfoItem" /> <variable name="vmInfo" type="InfoItem" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:text="@{vmInfo.title}" android:textColor="@color/black" android:textSize="20sp" tools:text="Balance" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" android:text="@{vmInfo.value}" tools:text="1000 $" /> </FrameLayout> </layout> <!----> <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="com.lfkekpoint.adapters.adapters.presentation.modules.bindableItemsSettings.SwitchItem" /> <variable name="vmSwitch" type="SwitchItem" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:text="@{vmSwitch.title}" android:textColor="@color/black" android:textSize="20sp" tools:text="Send notifications" /> <Switch android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" app:switchListener="@{vmSwitch.listener}" tools:checked="true" /> </FrameLayout> </layout> 

app: switchListener = "@ {vmSwitch.listener}" - nesta linha, usamos nosso BindingAdapter


* Nota: Por motivos justos, pode parecer que alguns escrevemos muito mais código em xml - mas isso é uma questão de conhecer a biblioteca de ligação de dados do Android. Complementa o layout, lê rapidamente e, em princípio, remove em grande parte precisamente o padrão. Acho que o Google vai desenvolver bem essa biblioteca, pois é a primeira na guia Arquitetura do Android Jetpack. Tente mudar o MVP para o MVVM em alguns projetos - e muitos podem ser agradavelmente surpreendidos.

Bem, então! .. Ah, o código em SettingsActivity:

SettingsActivity.kt
... não mudou, a menos que o adaptador tenha mudado! =) Mas para não pular o artigo:

 class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) val adapter = BaseVmListAdapter() rView.layoutManager = LinearLayoutManager(this) rView.adapter = adapter adapter.add(InfoItem("User Name", "Leo Allford")) adapter.add(InfoItem("Balance", "350 $")) adapter.add(InfoItem("Tariff", "Business")) adapter.add(SwitchItem(1, "Send Notifications") { itemId, userChoice -> onCheck(itemId, userChoice) }) adapter.add(SwitchItem(2, "Send News on Email") { itemId, userChoice -> onCheck(itemId, userChoice) }) } private fun onCheck(itemId: Int, userChoice: Boolean) { when (itemId) { 1 -> Toast.makeText(this, "Notification now set as $userChoice", Toast.LENGTH_SHORT).show() 2 -> Toast.makeText(this, "Send news now set as $userChoice", Toast.LENGTH_SHORT).show() } } } 

Sumário


Temos um algoritmo para criar listas e ferramentas para trabalhar com eles. No meu caso (quase sempre uso a ligação de dados ), toda a preparação se resume em inicializar as classes base em pastas, layout dos itens em .xml e, em seguida, vincular as variáveis ​​em .kt.

Acelerando o desenvolvimento
Para um trabalho mais rápido, usei os modelos do Apache para Android Studio - e escrevi meus modelos com uma pequena demonstração de como tudo funciona. Eu realmente espero que alguém seja útil. Observe que, ao trabalhar, você precisa chamar o modelo na pasta raiz do projeto - isso é feito porque o parâmetro applicationId do projeto pode estar para você se você o tiver alterado em Gradle. Mas packageName não pode ser enganado tão facilmente, o que eu usei. O idioma disponível sobre o modelo pode ser lido nos links abaixo

Referências / Mídia


1. Desenvolvimento moderno do Android: Android Jetpack, Kotlin e mais (Google I / O 2018, 40 m.) - um breve guia sobre o que está na moda hoje em dia. A partir daqui, também ficará claro em termos gerais como o RecyclerView se desenvolveu;

2. Droidcon NYC 2016 - Radical RecyclerView, 36 m. - Um relatório detalhado sobre o RecyclerView de Lisa Wray ;

3. Crie uma lista com o RecyclerView - documentação oficial

4. Interfaces vs. aulas

5. Manual do FreeMarker - Formato de modelo do IDE Android, modelo total , manual do FreeMarker - uma abordagem conveniente que, na estrutura deste artigo, ajudará a criar rapidamente os arquivos necessários para trabalhar com listas

6. Código do artigo (existem nomes de classe ligeiramente diferentes, tenha cuidado), modelos para trabalho e vídeo, como trabalhar com modelos

7. Versão em inglês do artigo

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


All Articles