Seis meses se passaram desde que saí de Pascalem kotlin e me apaixonei pelo desenvolvimento do Android, e agora já me permito escalar publicamente com minhas ideias o mosteiro de outra pessoa. Mas há uma razão para isso. Tendo assistido em chats de perfil quais questões surgem com mais frequência para desenvolvedores Android, e não apenas para iniciantes, percebi que na maioria dos casos, quando uma pessoa encontra um erro que não consegue entender, pois não consegue entender a explicação dos colegas do chat ou suas principais questões, o motivo é o uso impensado de partes de código ou bibliotecas prontas. No entanto, confiar em exemplos de código prontos que não funcionam para eles (e nesta área, o código escrito há mais de um ano, por padrão, requer atualização ou retrabalho geral, e isso se aplica a código com estouro de pilha, guias de biblioteca , e mesmo guias do próprio Google), eles não entendem os motivos dos erros ocorridos ou o comportamento diferente,desde que conte com uma biblioteca comoSala chinesa , sem tentar compreender a sua arquitectura e princípios de trabalho.
Uma vez que os problemas da visualização do reciclador surgem com frequência, gostaria de entender um pouco sobre como fazer um código extensível e limpo sozinho para exibir uma lista de vários itens em um aplicativo.
Estudando os padrões arquitetônicos de desenvolvimento do Android, treinei-me para primeiro procurar respostas no servidor de guias do desenvolvedor do Google . Mas às vezes lá, especialmente nos codelabs de treinamento, há exemplos de código que são mais simplificados do que projetados para versatilidade, pureza e extensibilidade.
Nesse caso, precisei usar uma visualização sofisticada do reciclador para exibir uma lista de itens com marcações e lógicas internas diferentes. Todos os aplicativos modernos são baseados nesta ideia - de mensageiros instantâneos e feeds de mídia social a aplicativos bancários. Além disso, combinar em tempo real usando uma abordagem reativa de diferentes elementos visuais da lista de visualização do reciclador em vez da marcação de layout manual é uma ponte para o mundo da interface do usuário declarativa-funcional, que é oferecida a nós no Jetpack Compose, e que mais cedo ou mais tarde, o Google gentilmente oferecerá a troca.
Codelab, recycler view , sealed . . , ,- , , . , /, ( , SOLID, ).
, Google id data- : id Long.MIN_VALUE, id data-. : data-, , . recycler view .
. adapter delegates, groupie epoxy. , . , , . , , , .
:
, , 10%, ;
: , - data- .
, , , , , , .
, , , , recycler view , . , .
, .
recycler view, ListAdapter, , :
getItemType - , ( , Google );
onCreateViewHolder - , ViewHolder , ( );
onBindViewHolder - , ( ) ViewHolder, .
recycler view , recycler view , , , , DiffUtil-.
DiffCallback,
class BaseDiffCallback : DiffUtil.ItemCallback<HasStringId>() {
override fun areItemsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HasStringId, newItem: HasStringId): Boolean = oldItem == newItem
}
, areContentsTheSame , areItemsTheSame true. HasStringId, id String equals, data- , view. Data- id, DiffUtil , ui- id .
, , . , :
interface ViewHoldersManager {
fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor)
fun getItemType(item: Any): Int
fun getViewHolder(itemType: Int): ViewHolderVisitor
}
recycler view:
object ItemTypes {
const val UNKNOWN = -1
const val HEADER = 0
const val TWO_STRINGS = 1
const val ONE_LINE_STRINGS = 2
const val CARD = 3
}
"" adapter delegates, . .
hilt data binding, : ui. , , :
@Module
@InstallIn(FragmentComponent::class)
object DiModule {
@Provides
@FragmentScoped
fun provideAdaptersManager(): ViewHoldersManager = ViewHoldersManagerImpl().apply {
registerViewHolder(ItemTypes.HEADER, HeaderViewHolder())
registerViewHolder(ItemTypes.ONE_LINE_STRINGS, OneLine2ViewHolder())
registerViewHolder(ItemTypes.TWO_STRINGS, TwoStringsViewHolder())
registerViewHolder(ItemTypes.CARD, CardViewHolder())
}
}
:
ard item
<?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>
<variable name="card" type="ru.alexmaryin.recycleronvisitor.data.ui_models.CardItem" />
</data>
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="8dp"
card_view:cardBackgroundColor="@color/cardview_shadow_end_color"
card_view:cardCornerRadius="15dp">
<ImageView
android:id="@+id/card_background_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@android:mipmap/sym_def_app_icon" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:drawable/screen_background_dark_transparent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textAllCaps="true"
android:textColor="#FFFFFF"
android:textStyle="bold"
tools:text="Cart title"
android:text="@{card.title}"/>
<TextView
android:id="@+id/txt_discription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#FFFFFF"
tools:text="this is a simple discription with losts of text lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
android:text="@{card.description}"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>
One line item
<?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>
<variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.OneLineItem2" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:text="@{model.left}"
android:textAlignment="textEnd"
android:textAppearance="?attr/textAppearanceListItem"
android:textColor="@color/cardview_dark_background"
app:layout_constraintEnd_toStartOf="@+id/divider"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry,TextContrastCheck"
tools:text="Left text" />
<ImageView
android:id="@+id/divider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.6"
android:padding="5dp"
android:scaleType="center"
android:scaleX="0.5"
android:scaleY="0.9"
android:src="@drawable/ic_outline_waves_24"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/text1"
app:layout_constraintEnd_toStartOf="@+id/text2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/text1"
app:layout_constraintTop_toTopOf="@+id/text1"
app:srcCompat="@drawable/ic_outline_waves_24"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<TextView
android:id="@id/text2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingEnd="8dp"
android:text="@{model.right}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="@+id/divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/divider"
app:layout_constraintTop_toTopOf="@+id/divider"
tools:ignore="RtlSymmetry"
tools:text="Right text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Two line item
<?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">
<data>
<variable name="model" type="ru.alexmaryin.recycleronvisitor.data.ui_models.TwoStringsItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeight"
android:mode="twoLine"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
<TextView
android:id="@+id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{model.caption}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="@id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{model.details}"
app:layout_constraintTop_toBottomOf="@id/text1"
app:layout_constraintStart_toStartOf="parent"
android:textAppearance="?attr/textAppearanceListItemSecondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Header item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="headerItem"
type="ru.alexmaryin.recycleronvisitor.data.ui_models.RecyclerHeader" />
</data>
<TextView
style="@style/regularText"
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#591976D2"
android:textAlignment="center"
android:textStyle="italic"
android:text="@{headerItem.text}"/>
</layout>
, :
interface ViewHolderVisitor {
val layout: Int
fun acceptBinding(item: Any): Boolean
fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById)
}
( acceptVisitor execute, , ) - acceptBinding bind, layout, .
accept : ( ) , , , accept, , true. , , , . - (accept = true), - , .
, , . :
class ViewHoldersManagerImpl : ViewHoldersManager {
private val holdersMap = emptyMap<Int, ViewHolderVisitor>().toMutableMap()
override fun registerViewHolder(itemType: Int, viewHolder: ViewHolderVisitor) {
holdersMap += itemType to viewHolder
}
override fun getItemType(item: Any): Int {
holdersMap.forEach { (itemType, holder) ->
if(holder.acceptBinding(item)) return itemType
}
return ItemTypes.UNKNOWN
}
override fun getViewHolder(itemType: Int) = holdersMap[itemType] ?: throw TypeCastException("Unknown recycler item type!")
}
( ):
class CardViewHolder : ViewHolderVisitor {
override val layout: Int = R.layout.card_item
override fun acceptBinding(item: Any): Boolean = item is CardItem
override fun bind(binding: ViewDataBinding, item: Any, clickListener: AdapterClickListenerById) {
with(binding as CardItemBinding) {
card = item as CardItem
Picasso.get().load(item.image).into(cardBackgroundImage)
}
}
}
as . -, , : accept , CardItem, bind . : layout, binding data binding . -, , idea android studio ?
, recycler view,- , , , :
class BaseListAdapter(
private val clickListener: AdapterClickListenerById,
private val viewHoldersManager: ViewHoldersManager
) : ListAdapter<HasStringId, BaseListAdapter.DataViewHolder>(BaseDiffCallback()) {
inner class DataViewHolder(
private val binding: ViewDataBinding,
private val holder: ViewHolderVisitor
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: HasStringId, clickListener: AdapterClickListenerById) =
holder.bind(binding, item, clickListener)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =
LayoutInflater.from(parent.context).run {
val holder = viewHoldersManager.getViewHolder(viewType)
DataViewHolder(DataBindingUtil.inflate(this, holder.layout, parent, false), holder)
}
override fun onBindViewHolder(holder: DataViewHolder, position: Int) = holder.bind(getItem(position), clickListener)
override fun getItemViewType(position: Int): Int = viewHoldersManager.getItemType(getItem(position))
}
view, :
// - :
// private val viewModel: MainViewModel by viewModels()
// private lateinit var recycler: RecyclerView
// @Inject lateinit var viewHoldersManager: ViewHoldersManager
// private val items = mutableListOf<HasStringId>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recycler = requireActivity().findViewById(R.id.recycller)
val itemsAdapter = BaseListAdapter(AdapterClickListenerById {}, viewHoldersManager)
itemsAdapter.submitList(items)
recycler.apply {
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(DividerItemDecoration(requireContext(), (layoutManager as LinearLayoutManager).orientation))
adapter = itemsAdapter
}
populateRecycler()
}
private fun populateRecycler() {
lifecycleScope.launch {
viewModel.getItems().flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { items.add(it) }
}
}
"" , recycler view . :
-;
sealed ;
data- / , view data ;
- ;
, SOLID ;
, (YAGNI).
Claro, minha implementação ainda tem maneiras de melhorar e expandir. Você pode, como em groupie, adicionar agrupamento de elementos e seu colapso visual. Você pode abandonar a vinculação de dados ou complementar o adaptador com opções para uma vinculação de visão ou um aumento de marcação regular com todos os seus findViewById favoritos em suportes de visão. E então o código se transformará na mesma biblioteca, da qual já existem tantos e assim. Para os meus propósitos específicos, no momento em que surgiu a necessidade, a opção com um simples Visitante é mais do que suficiente:
Por favor, não julgue estritamente, já que este é meu primeiro nascimento no mundo andróide. O código de exemplo completo do texto do artigo estará disponível no repositório github .