Bonjour à tous! Mes messages sont un désir d'aider avec certains éléments Android. Si vous êtes un développeur qui n'a pas encore développé d'algorithme pour créer des listes, vous trouverez peut-être utile de lire ce document. Fondamentalement, je voudrais proposer des solutions de développement prêtes à l'emploi, révélant au cours de l'histoire quelques réflexions sur la façon dont je suis
arrivé à
elles .
Dans cet article:- nous formons plusieurs classes de base et interfaces pour travailler avec RecyclerView et RecyclerView.Adapter
- connecter une bibliothèque à partir d'Android Jetpack (facultatif, d'abord sans)
- pour un développement encore plus rapide - l'option de modèle à la fin de l'article;)
Entrée
Eh bien! Tout le monde a déjà oublié ListView et écrit en toute sécurité sur RecyclerView (
Rv ). Ces moments où nous avons
nous -
mêmes implémenté le modèle
ViewHolder sont
tombés dans l'oubli.
Rv nous fournit un ensemble de classes prêtes à l'emploi pour implémenter des listes et une assez grande sélection de
LayoutManagers pour les afficher. En fait, en regardant de nombreux écrans, vous pouvez imaginer la plupart d'entre eux comme une liste - précisément en raison de la capacité de chaque élément à implémenter son propre
ViewHolder .
On nous a raconté l'histoire du développement plus en détail
sur Google I / O.Mais, il y a toujours quelques «mais!». Les réponses standard à Stackoverflow suggèrent des solutions générales qui conduisent à copier-coller, en particulier à l'endroit où l'adaptateur est implémenté.
À l'heure actuelle,
Rv a déjà trois ans. Il y a beaucoup d'informations à ce sujet, et il existe de nombreuses bibliothèques avec des solutions prêtes à l'emploi, mais que se passe-t-il si vous n'avez pas besoin de toutes les fonctionnalités, ou si vous montez pour regarder le code de quelqu'un d'autre - et vous voyez là l'
Ancien Horreur n'est pas ce que vous aimeriez voir, ou pas quoi imaginé? Au cours de ces trois années, Android a finalement officiellement accepté Kotlin = la lisibilité du code s'est améliorée, de nombreux articles intéressants ont été publiés sur
Rv qui révèlent pleinement ses capacités.
Le but de ceci est de recueillir la base des meilleures pratiques de
votre vélo , un cadre pour travailler avec des listes pour de nouvelles applications. Ce cadre peut être complété par une logique d'application en application, en utilisant ce dont vous avez besoin et en éliminant les inutiles. Je pense que cette approche est bien meilleure que la bibliothèque de quelqu'un d'autre - dans vos cours, vous avez la possibilité de comprendre comment tout fonctionne et de contrôler les cas dont vous avez besoin sans être lié à la décision de quelqu'un d'autre.
Réfléchissons logiquement et dès le début
Ce que le composant doit faire sera décidé par l'
interface, pas par la classe , mais à la fin, nous fermerons la logique d'implémentation concrète à la classe qui implémentera et implémentera cette interface. Mais, s'il s'avère qu'avec l'implémentation de l'interface un copier-coller se forme - on peut le cacher derrière une classe abstraite, et après - une classe qui hérite de l'abstrait. Je vais montrer ma mise en œuvre des interfaces de base, mais mon objectif est que le développeur essaie juste de penser dans la même direction. Encore une fois - le plan est le suivant:
Un ensemble d'interfaces -> une classe abstraite qui prend du copier-coller (si nécessaire) -> et déjà une classe spécifique avec un code unique . Vous pouvez implémenter les interfaces différemment.
Que peut faire un adaptateur avec une liste? La réponse à cette question est plus facile à obtenir lorsque vous regardez un exemple. Vous pouvez jeter un œil à RecyclerView.Adapter, vous y trouverez quelques conseils. Si vous réfléchissez un peu, vous pouvez imaginer quelque chose comme ces méthodes:
IBaseListAdapterinterface IBaseListAdapter<T> { fun add(newItem: T) fun add(newItems: ArrayList<T>?) fun addAtPosition(pos : Int, newItem : T) fun remove(position: Int) fun clearAll() }
* En parcourant les projets, j'ai trouvé plusieurs autres méthodes que je vais omettre ici, par exemple getItemByPos (position: Int), ou même subList (startIndex: Int, endIndex: Int). Je le répète: vous devez vous-même regarder ce dont vous avez besoin dans le projet et inclure des fonctions dans l'interface. Ce n'est pas difficile quand on sait que tout se passe en une seule classe. L'ascétisme dans ce domaine vous permettra de vous débarrasser de la logique inutile, ce qui dégrade la lisibilité du code, car une implémentation particulière prend plus de lignes. Faites attention au générique
T. En général, l'adaptateur fonctionne avec n'importe quel objet de la liste (élément), il n'y a donc aucune clarification ici - nous n'avons pas encore choisi notre approche. Et dans cet article, il y en aura au moins deux, la première interface ressemble à ceci:
interface IBaseListItem { fun getLayoutId(): Int }
Eh bien, oui, cela semble logique - nous parlons d'un élément de liste, donc chaque élément doit avoir une sorte de mise en page, et vous pouvez vous y référer en utilisant layoutId. Très probablement, un développeur novice n'aura probablement besoin de rien, à moins bien sûr que nous adoptions
des approches plus
avancées . Si vous avez suffisamment d'expérience en développement, vous pouvez certainement faire un délégué ou un wrapper, mais cela vaut-il la peine avec un petit projet - et encore moins d'expérience en développement? Tous mes liens quelque part sur YouTube sont très utiles si vous n'avez pas le temps en ce moment - souvenez-vous-en et lisez la suite, car l'approche est plus simple ici - je pense qu'avec le travail standard avec
Rv ,
à en juger par la documentation officielle , ce qui est proposé ci-dessus n'est pas implicite.
Il est temps de combiner notre
IBaseListAdapter avec des interfaces, et la classe suivante sera abstraite:
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() } }
* Remarque: Faites attention à la fonction
getItemViewType (position: Int) remplacée. Nous avons besoin d'une sorte de clé int, par laquelle Rv comprendra quel ViewHolder nous convient.
L'agencement de
ValId pour notre
article est très utile pour cela, car Android à chaque fois rend l'id des mises en page unique et toutes les valeurs sont supérieures à zéro - nous l'utiliserons plus tard pour «
gonfler »
itemView pour nos téléspectateurs dans la méthode
inflateByViewType () (ligne suivante).
Créer une liste
Prenons par exemple l'écran des paramètres. Android nous propose
sa propre version, mais que se passe-t-il si le design a besoin de quelque chose de plus sophistiqué? Je préfère remplir cet écran comme une liste. Voici un tel cas:

Nous voyons deux éléments de liste différents, donc
SimpleListAdapter et
Rv sont parfaits ici!
Commençons! Vous pouvez commencer par les dispositions de mise en page pour 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>
Ensuite, nous définissons les classes elles-mêmes, à l'intérieur desquelles nous voulons passer les valeurs qui interagissent avec la liste: la première est l'en-tête et une valeur qui vient de l'extérieur (nous aurons un talon, à propos des demandes une autre fois), la seconde est l'en-tête et la variable booléenne par lequel nous devons effectuer une action. Pour distinguer les éléments Switch, l'id des entités du serveur convient, s'ils ne sont pas là, nous pouvons les créer nous-mêmes lors de l'initialisation.
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 }
Dans une implémentation simple, chaque élément aura également besoin d'un 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 }
Eh bien, la partie la plus intéressante est l'implémentation concrète de 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") } } }
* Remarque: N'oubliez pas que sous le capot de la méthode
inflateByViewType (contexte, viewType, parent): viewType = layoutId.Tous les composants sont prêts! Maintenant, le code d'activité reste et vous pouvez exécuter le programme:
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() } } }
Par conséquent, lors de la création de la liste, tout le travail se résume à ce qui suit:
1. Calcul du
nombre de dispositions différentes pour les articles
2. Choisissez leurs
noms . J'utilise la règle:
Something Item.kt, item_
something .xml,
Something ViewHolder.kt
3. Nous écrivons un
adaptateur pour ces classes. En principe, si vous ne prétendez pas optimiser, un seul adaptateur commun suffit. Mais dans les grands projets, j'en ferais encore quelques-uns, sur les écrans, car dans le premier cas la méthode
onBindViewHolder () se développe inévitablement (la lisibilité du code en souffre) dans votre adaptateur (dans notre cas, c'est
SettingsListAdapter ) + le programme devra aller à chaque fois, pour chaque élément, exécuter sur cette méthode + méthode
onCreateViewHolder ()4. Exécutez le code et profitez-en!
Jetpack
Jusqu'à ce moment, nous
utilisions l' approche de liaison de données standard de
Item.kt à notre
item_layout.xml . Mais nous pouvons unifier la méthode
onBindViewHolder () , la laisser minimale et transférer la logique vers Item et layout.
Allons à la page officielle Android JetPack:

Prenons attention au premier onglet de la section Architecture.
La liaison de données Android est un sujet très vaste, je voudrais en parler plus en détail dans d'autres articles, mais pour l'instant nous ne l'utiliserons que dans le cadre de l'actuel - nous ferons notre
Item.kt -
variable pour
item.xml (ou vous pouvez l'appeler un modèle de vue pour la mise en page).
Au moment de l'écriture, la
liaison de données pouvait être connectée comme ceci:
android { compileSdkVersion 27 defaultConfig {...} buildTypes {...} dataBinding { enabled = true } dependencies { kapt "com.android.databinding:compiler:3.1.3"
Reprenons les classes de base. L'interface de l'article complète la précédente:
interface IBaseItemVm: IBaseListItem { val brVariableId: Int }
De plus, nous étendrons notre ViewHolder, par conséquent, nous sommes contactés avec la databyding. Nous lui passerons
ViewDataBinding , après quoi nous oublierons en toute sécurité de créer une mise en page et une liaison de données
class VmViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
La même approche est utilisée
ici , mais sur Kotlin, elle semble beaucoup plus courte, n'est-ce pas? =)
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) } }
Faites généralement attention aux méthodes
onCreateViewHolder () ,
onBindViewHolder () . L'idée est qu'ils ne grandissent plus. Au total, vous obtenez un adaptateur pour n'importe quel écran, avec tous les éléments de la liste.
Nos articles:
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) } }
Ici, vous pouvez voir où la logique de la méthode
onBindViewHolder () est allée . Android Databinding l'a pris sur lui-même - maintenant n'importe laquelle de nos mises en page est sauvegardée par son modèle de vue, et elle traitera calmement toute la logique des clics, des animations, des requêtes et d'autres choses. Que proposez-vous.
Les adaptateurs de liaison feront un bon travail en vous permettant de connecter la vue à des données de toute nature. la communication peut également être améliorée grâce à la
databyding bidirectionnelle . Il clignotera probablement dans l'un des articles suivants, dans cet exemple, tout peut être simplifié. Un seul adaptateur nous suffit:
@BindingAdapter("switchListener") fun setSwitchListener(sw: Switch, listener: CompoundButton.OnCheckedChangeListener) { sw.setOnCheckedChangeListener(listener) }
Après cela, nous lions nos valeurs variables avec notre
élément dans le 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}" - dans cette ligne, nous avons utilisé notre
BindingAdapter * Remarque: Pour des raisons valables, il peut sembler à certains que nous écrivons beaucoup plus de code en xml - mais il s'agit de connaître la bibliothèque Android Databinding. Il complète la disposition, lit rapidement et, en principe, supprime en grande partie le passe-partout. Je pense que Google va bien développer cette bibliothèque, car c'est la première dans l'onglet Architecture d'Android Jetpack. Essayez de changer MVP en MVVM dans quelques projets - et beaucoup pourraient être agréablement surpris.
Eh bien! Ah, le code dans SettingsActivity:
SettingsActivity.kt... n'a pas changé, sauf si l'adaptateur a changé! =) Mais pour ne pas sauter l'article:
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() } } }
Résumé
Nous avons obtenu un algorithme pour construire des listes et des outils pour travailler avec eux. Dans mon cas (j'utilise presque toujours la
liaison de données ), toute la préparation se résume à l'initialisation des classes de base dans les dossiers, la disposition des éléments en .xml, puis la liaison aux variables en .kt.
Accélérer le développementPour un travail plus rapide, j'ai utilisé les
modèles d'Apache pour Android Studio - et écrit
mes modèles avec une petite
démonstration de la façon dont tout cela fonctionne. J'espère vraiment que quelqu'un vous sera utile. Veuillez noter que lorsque vous travaillez, vous devez appeler le modèle à partir du dossier racine du projet - cela est fait car le paramètre
applicationId du projet peut vous mentir si vous le modifiez dans Gradle. Mais
packageName ne peut pas être
dupe si facilement, ce que j'ai utilisé. La langue disponible sur le modèle peut être lue sur les liens ci-dessous
Références / Médias
1.
Développement Android moderne: Android Jetpack, Kotlin, et plus (Google I / O 2018, 40 m.) - un petit guide de ce qui est à la mode aujourd'hui, à partir d'ici, il sera également clair en termes généraux comment RecyclerView a développé;
2.
Droidcon NYC 2016 - Radical RecyclerView, 36 m. - Un rapport détaillé sur RecyclerView de
Lisa Wray ;
3.
Créez une liste avec RecyclerView - documentation officielle
4.
Interfaces vs cours5.
Format de modèle IDE Android, modèle total ,
manuel FreeMarker - une approche pratique qui, dans le cadre de cet article, aidera à créer rapidement les fichiers nécessaires pour travailler avec des listes
6.
Code de l'article (il y a des noms de classe légèrement différents, faites attention), des
modèles pour le travail et la
vidéo, comment travailler avec des modèles7. Version
anglaise de l'article