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,
Rv já
tem 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:
IBaseListAdapterinterface 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 desenvolvimentoPara 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. aulas5.
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 modelos7. Versão em
inglês do artigo