📜 ⬆️ ⬇️

Development for Android. A bit about fast work with lists

Hello! My posts - the desire to help in working with some elements of Android. If you are a developer who has not yet formed an algorithm for building lists for himself - it may be useful for you to read this material. Basically, I would like to offer ready-made solutions for development, revealing in the course of the narration some thoughts about how I came to them .

In this article:


Introduction


Well! Everyone has already forgotten about ListView and safely writes to RecyclerView ( Rv ). The times when we implemented the ViewHolder pattern themselves have sunk into oblivion. Rv provides us with a set of ready-made classes for implementing lists and a fairly large selection of LayoutManagers for displaying them. In fact, looking at the many screens, the list can provide most of them - precisely because of the opportunity for each element to implement its ViewHolder . For more details on the history of development, we were told on Google I / O.
')
But, there is always a pair of “but”! .. Standard answers to Stackoverflow suggest common solutions that lead to copy-paste, especially in the place of implementation of the Adapter.

At the moment, Rv is already three years old. Infa is a cloud over it, and there are many libraries with ready-made solutions, but what to do if you don’t need all the functionality, or if you climb to look at someone else’s code - and you see there Ancient Horror is not what you would like to see, or not that at all imagined? During these three years, Android finally finally took Kotlin to itself = improved readability of the code, a lot of interesting articles appeared on Rv that fully reveal its capabilities.

The purpose of this is to collect from the best practices your bike basis, the framework for working with lists for new applications. This framework can be supplemented with logic from application to application, using what you need, and discarding unnecessary. I think this approach is much better than someone else's library - in your classes you have the opportunity to figure out how everything works, and to control the cases you need without being tied to someone else's decision.

Let's think logically and right from the start.


We will decide what the component should do, the interface, not the class , but at the end we will close the specific implementation logic to the class that will implement and implement this interface. But, if it turns out that when an interface is implemented, a copy-paste is formed - we can hide it behind the abstract class, and after it - a class that inherits from the abstract. I’ll show my implementation of the base interfaces, but my goal is for the developer to just try to think in the same direction. Once again - the plan is this: A set of interfaces -> abstract class that takes copy-paste (if necessary) -> and already a specific class with unique code . You can implement interfaces differently.

What can an adapter do with the list? The answer to this question is easiest to get when you look at some kind of example. You can look at the RecyclerView.Adapter, you will find a couple of tips. If you think a little, you can imagine something like this:

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() } 

* Going through projects, I found several other methods that I’ll omit here, for example getItemByPos (position: Int), or even subList (startIndex: Int, endIndex: Int). I repeat: you yourself have to look at what you need from the project and include functions in the interface. It is not difficult when you know that everything happens in one class. Asceticism in this matter will allow to get rid of unnecessary logic, which impairs the readability of the code, because a specific implementation takes more lines.

Pay attention to generic T. In general, the adapter works with any list object (item), so there is no clarification here - we have not yet chosen our approach. And in this article there will be at least two, the first interface looks like this:

 interface IBaseListItem { fun getLayoutId(): Int } 

Well, yes, it seems logical - we are talking about a list element, which means that each element must have some kind of layout, and you can refer to it using layoutId. More than anything a novice developer probably will not need, unless of course you take more advanced approaches . If you have enough experience in development, you can of course make a delegate or a wrapper, but is it worth it with a small project - and even less development experience? All my links somewhere in YouTube are very useful, if you don’t have time now - just memorize them and read further, because here the approach is simpler - I think that with standard work with Rv , judging by the official documentation , what is offered above is not implied.

It's time to combine our IBaseListAdapter with interfaces, and the following class will be abstract:

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() } } 

* Note: Note the overridden function getItemViewType (position: Int) . We need some kind of new key, according to which Rv will understand which ViewHolder suits us. ValidId for our item is very useful for this; The Android every time helpsfully makes the layout id unique, and all values ​​are greater than zero - we will use this further, “inflating” itemView for our viewholders in the inflateByViewType () method (next line).

Create a list


Take for example the settings screen. Android offers us its own version, but what if the design needs something more sophisticated? I prefer to fill this screen as a list. There will be given such a case:



We see two different elements of the list, so SimpleListAdapter and Rv are perfect here!

Let's get started! You can start with layout layouts for 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> 

Then, we define the classes themselves, into which we want to pass the values ​​that interact with the list: the first is the header and any value that comes from the outside (we will have a stub about requests another time), the second is the header and boolean variable , by which we must perform an action. To distinguish between Switch elements, id entities from the server will do; if they are not there, we can create them ourselves during initialization.

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 } 

In a simple implementation, each element will also need a 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 } 

Well, the most interesting part is the concrete implementation of 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") } } } 

* Note: Do not forget that under the hood of the inflateByViewType method (context, viewType, parent): viewType = layoutId.

All components are ready! Now, the Activation code remains and you can run the program:

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() } } } 

As a result, when building a list, all the work comes down to the following:

1. Calculate the number of different layouts for the items

2. Choose their name . I use the rule: Something Item.kt, item_ something .xml, Something ViewHolder.kt

3. We write an adapter to these classes. In principle, if you do not pretend to optimize, one common adapter will suffice. But in large projects, I would still do a few on the screens, because in the first case the onBindViewHolder () method inevitably grows (code readability suffers) in your adapter (in our case, the SettingsListAdapter ) + program will have each time for each item, go over this method + onCreateViewHolder ()

4. Run the code and enjoy!

Jetpack


Up to this point, we have applied the standard data binding approach from Item.kt - to our item_layout.xml . But we can unify the onBindViewHolder () method, leave it minimal, and transfer the logic to Item and layout.

Head over to the official Android JetPack page:



Let's pay attention to the first tab in the Architecture section. Android Databinding is a very extensive topic, I would like to talk about it in more detail in other articles, but now we will use only the current one - we will make our Item.kt - variable for item.xml (or you can call it a view model for the layout).

At the time of this writing, Databinding could be connected like this:

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

Go through the base classes again. The interface for the item complements the previous one:

 interface IBaseItemVm: IBaseListItem { val brVariableId: Int } 

Also, we will expand our ViewHolder, so we contacted databiding. We will pass the ViewDataBinding into it, after which we safely forget about creating the layout and data binding

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

The same approach is used here , but on Cotlin it looks much shorter, doesn't it? =)

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) } } 

In general, pay attention to the onCreateViewHolder () , onBindViewHolder () methods. The idea is that they no longer grow. So you get one adapter for any screen, with any list items.

Our items:

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) } } 

Here you can see where the logic of the onBindViewHolder () method has gone . Android Databinding took over it - now any of our layout is supported by its view model, and it will calmly handle all the logic of clicks, animations, requests and other things. What do you come up with. Binding Adapters will be a good help in this - allowing you to link the view to data of any kind. Also, communication can be improved through bilateral dating . Probably he will flash in any of the following articles, in this example, you can make everything easier. We only need one adapter adapter:

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

After that, we associate our variable values ​​with our Item inside 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}" - in this line we used our BindingAdapter


* Note: For fair reasons, it may seem to someone that we are writing a lot more code in xml - but this is a question of knowledge of the Android Databinding library. It complements the layout, reads quickly and, in principle, for the most part removes the boilerplate. I think Google is going to develop this library well, since it is the first in the Architecture tab in Android Jetpack. Try changing MVP to MVVM in a couple of projects - and many may be pleasantly surprised.

Well then! .. Ah, the code in SettingsActivity:

SettingsActivity.kt
... has not changed, except that the adapter has changed! =) But in order not to jump on the 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() } } } 

Total


We received an algorithm for building lists and tools for working with them. In my case (I almost always use Databinding ) all the preparation is reduced to initializing the base classes into folders, layout of the items in .xml and then binding to variables in .kt.

Accelerate development
For faster work, I used Apache for Android Studio templates - and wrote my templates with a small demonstration of how it all works. I really hope that someone will come in handy. Please note that during the work you need to call the template from the root folder of the project - this is done because the project applicationId parameter can lie to you if you changed it in Gradle. But packageName is not so easy to catch, which I used. Available language about templating can be found at the links below.

References / Media


1. Modern Android development: Android Jetpack, Kotlin, and more (Google I / O 2018, 40 m.) - a short guide to what is in fashion today, from here also in general terms, it will be clear how RecyclerView has developed;

2. Droidcon NYC 2016 - Radical RecyclerView, 36 m. - A detailed report on RecyclerView from Lisa Wray ;

3. Create a List with RecyclerView - official documentation

4. Interfaces vs. classes

5. Android IDE Template Format , Total Standardization , the FreeMarker manual - a convenient approach that within the framework of this article will help you quickly create the necessary files for working with lists

6. The code for the article (there are slightly different class names, be careful), templates for work and video, how to work with templates

7. Version of the article in English

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


All Articles