Разработка под Android. Немного о быстрой работе со списками

Всем привет! Мои посты — желание помочь в работе с какими-то элементами Android. Если вы разработчик, который еще не сформировал для себя алгоритм для построения списков — вам может оказаться полезным почитать этот материал. В основном, я бы хотел предлагать готовые решения для разработки, раскрывая в ходе повествования какие-то мысли о том, как я до них докатился к этому пришел.

В этой статье:

  • формируем несколько базовых классов и интерфейсов для работы с RecyclerView и RecyclerView.Adapter
  • подключим одну библиотеку из Android Jetpack (по желанию, сначала без нее)
  • для еще более быстрой разработки — вариант темплейта в конце статьи ;)

Вступление


Ну что ж! Все уже забыли про ListView и благополучно пишут на RecyclerView(Rv). Те времена, когда мы реализовывали сами паттерн ViewHolder, канули в небытие. Rv предоставляет нам набор готовых классов для реализации списков и достаточно большой выбор LayoutManager'ов для их отображения. По сути, глядя на множество экранов, списком можно представить большинство из них — именно благодаря возможности для каждого элемента реализовать свой ViewHolder. Более подробно историю развития нам рассказали на Google I/O.

Но, всегда есть пара «но»!.. Стандартные ответы на Stackoverflow подсказывают общие решения, которые приводят к копипасте, особенно в месте реализации Adapter'a.

На данный момент, Rv уже три года. Инфы по нему туча, и много библиотек с готовыми решениями, но что делать, если вам не нужен весь функционал, или если вы залазите поглядеть на чужой код — и видите там Древний Ужас не то, что хотели бы видеть, или не то что вообще себе представляли? За эти три года Android наконец-таки официально принял к себе Kotlin = улучшилась читаемость кода, по Rv вышло немало интересных статей, которые в полной мере раскрывают его возможности.

Цель этой — собрать из лучших практик свой велосипед основу, каркас по работе со списками для новых приложений. Этот каркас можно дополнять логикой от приложения к приложению, используя то, что вам необходимо, и отбрасывая лишнее. Я считаю такой подход намного лучше чужой библиотеки — в своих классах вы имеете возможность разобраться с тем, как все работает, и проконтролировать кейсы в которых нуждаетесь, не завязываясь на чужом решении.

Давайте мыслить логично и с самого начала


Решать то, что должен делать компонент, будет интерфейс, а не класс, но конкретную логику реализации в конце замкнем на классе, который будет этот интерфейс имплементировать и реализовывать. Но, если получится так, что при реализации интерфейса образуется копипаста — мы можем спрятать ее за абстрактным классом, а после него — класс, который наследуется от абстрактного. Я покажу свою реализацию базовых интерфейсов, но моя цель состоит в том, чтобы разработчик просто попробовал думать в этом же направлении. Еще раз — план такой: Набор интерфейсов -> абстрактный класс, забирающий копипасту (если это нужно) -> и уже конкретный класс с уникальным кодом. Реализацию интерфейсов Вы можете выполнить по другому.

Что может делать со списком адаптер? Ответ на этот вопрос легче всего получить, когда смотришь на какой то пример. Можно заглянуть в RecyclerView.Adapter, вы найдете пару подсказок. Если же немного подумать, то можно представить примерно такие методы:

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

* Перебирая проекты, я нашел несколько других методов, которые здесь опущу, например getItemByPos(position: Int), или даже subList(startIndex: Int, endIndex: Int). Повторюсь: вы сами должны смотреть, что вам нужно от проекта, и включать функции в интерфейс. Это не сложно, когда знаешь что все происходит в одном классе. Аскетизм в данном вопросе позволит избавиться от лишней логики, которая ухудшает читаемость кода, потому что конкретная реализация занимает больше строк.

Обратите внимание на дженерик T. В общем случае, адаптер работает с любым объектом списка (item), поэтому здесь нет уточнения — мы еще не выбрали наш подход. А в этой статье их будет как минимум два, первый интерфейс выглядит так:

interface IBaseListItem {
    fun getLayoutId(): Int
}

Ну да, кажется логичным — мы же говорим об элементе списка, значит у каждого элемента должен быть какой-то лейаут, а сослаться на него можно с помощью layoutId. Больше ничего начинающему разработчику скорее всего не понадобится, если конечно не брать более продвинутые подходы. Если же у вас хватает опыта в разработке, можно конечно сделать делегат или обертку, но стоит ли оно того при небольшом проекте — и еще меньшем опыте разработки? Все мои ссылки куда то в ютуб очень полезны, если у вас сейчас нет времени — просто запомните их и читайте дальше, потому что здесь подход попроще — я считаю что при стандартной работе с Rv, судя по официальной документации, того что предлагается выше — не подразумевается.

Пора объединить наш IBaseListAdapter с интерфейсами, и следующий класс будет абстрактным:

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

*Примечание: Обратите внимание на переопределенную функцию getItemViewType(position: Int). Нам нужен некий интовый ключ, по которому Rv поймет, какой ViewHolder нам подходит. Для этого отлично пригодится val layoutId у нашей item, т.к. Андроид каждый раз услуживо делает id лейаутов уникальными, и все значения больше нуля — этим мы и воспользуемся далее, «надувая» itemView для наших вьюхолдеров в методе inflateByViewType() (следующая строка).

Создаем список


Возьмем для примера экран настроек. Андроид предлагает нам свой вариант, но что если по дизайну понадобится что-то более изощренное? Я предпочитаю наполнять этот экран как список. Тут будет приведен такой кейс:



Мы видим два разных элемента списка, значит SimpleListAdapter и Rv тут прекрасно подойдут!

Приступим! Можно начать с верстки лейаутов для item'ов:

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>

Затем, определяем сами классы, внутрь которых мы хотим передать значения, которые взаимодействуют со списком: первый — это заголовок и какое-либо значение, пришедшее извне (у нас будет заглушка, о запросах в другой раз), второй — это заголовок и boolean переменная, по которому мы должны выполнить действие. Чтобы различить Switch элементы, подойдут id сущностей с сервера, если же их нет — мы можем создать их сами при инициализации.

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
}

В простой реализации каждому элементу также понадобится 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
}

Ну и самая интересная часть — конкретная реализация 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")
        }
    }
}

*Примечание: Не забывайте про то, что под капотом метода inflateByViewType(context, viewType, parent): viewType = layoutId.

Все составляющие готовы! Теперь, остается код Активити и можно запускать программу:

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

В итоге, при построении списка, вся работа сводится к следующему:

1. Вычисление количества разных лейаутов для итемов

2. Подобрать им названия. Я пользуюсь правилом: SomethingItem.kt, item_something.xml, SomethingViewHolder.kt

3. Пишем к этим классам адаптер. В принципе, если вы не претендуете на оптимизацию, то хватит одного общего адаптера. Но в больших проектах я бы все же сделал несколько, по экранам, потому что в первом случае неизбежно разрастается метод onBindViewHolder() (страдает читаемость кода) в вашем адаптере (в нашем случае это SettingsListAdapter) + программе придется каждый раз, для каждого итема, пробегаться по этому методу + по методу onCreateViewHolder()

4. Запускаем код и радуемся!

JetPack


До этого момента мы применяли стандартный подход привязки данных из Item.kt — к нашему item_layout.xml. Но мы можем унифицировать метод onBindViewHolder(), оставить его минимальным, а логику перенести в Item и лейаут.

Зайдем на официальную страницу Android JetPack:



Обратим внимание на первую вкладку в разделе Architecture. Android Databinding — очень обширная тема, я бы хотел поговорить об ней более подробно в других статьях, но сейчас воспользуемся только в рамках текущей — мы сделаем нашу Item.ktvariable для item.xml (или можете назвать ее вьюмоделью для лейаута).

На момент написания статьи, Databinding можно было подключить вот так:

android {
    compileSdkVersion 27
    defaultConfig {...}
    buildTypes {...}

    dataBinding {
        enabled = true
    }

    dependencies {
        kapt "com.android.databinding:compiler:3.1.3"
        //... 
    }
 
}

Пройдемся заново по базовым классам. Интерфейс для итема дополняет предыдущий:

interface IBaseItemVm: IBaseListItem {
    val brVariableId: Int
}

Также, мы расширим наш ViewHolder, потому связались с датабайдингом. Будем передавать в него ViewDataBinding, после чего благополучно забудем о создании лейаута и привязке данных

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

Такой же подход используется здесь, но на котлине это выглядит намного короче, не правда ли? =)

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

Обратите внимание в целом на методы onCreateViewHolder(), onBindViewHolder(). Задумка в том, чтобы они больше не разрастались. Итого, вы получаете один адаптер для любого экрана, с любыми элементами списка.

Наши 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) }
}

Здесь становится видно, куда делась логика метода onBindViewHolder(). Ее взял на себя Android Databinding — теперь любой наш лейаут подкреплен своей вьюмоделью, и она спокойно обработает всю логику нажатий, анимаций, запросов и прочего. Что вы сами придумаете. В этом хорошо помогут Binding Adapters — позволив связать вью с данными любого рода. также, связь возможно улучшить благодаря двустороннему датабайдингу. Наверное он промелькнет в какой-нибудь из следующих статей, в данном примере можно сделать все проще. Нам достаточно одного байндинг адаптера:

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

После этого, связываем наши значения переменных с нашими Item внутри 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}" — в этой строке мы воспользовались нашим BindingAdapter'ом


*Примечание: Вполне по справедливым причинам, кое-кому может показаться, что мы пишем много больше кода в xml — но это вопрос знаний библиотеки Android Databinding. Она дополняет лейаут, быстро читается и в принципе по большей части убирает именно бойлерплейт. Я думаю, Google собирается хорошо развить эту библиотеку, раз она находится первой во вкладке Architecture, в Android Jetpack. Попробуйте в паре проектов сменить MVP на MVVM — и многие могут быть приятно удивлены.

Ну что ж!.. А, код в SettingsActivity:

SettingsActivity.kt
… не изменился, разве что поменялся адаптер! =) Но чтобы не прыгать по статье:

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

Итог


Мы получили алгоритм построения списков и инструменты для работы с ними. В моем случае (почти всегда использую Databinding) вся подготовка сводится к инициализации базовых классов по папкам, верстке итемов в .xml а затем привязке к переменным в .kt.

Ускоряем разработку
Для более быстрой работы, я воспользовался шаблонами от Apache для Android Studio — и написал свои темплейты с небольшой демонстрацией, как это все работает. Очень надеюсь, что кому-то пригодится. Обратите внимание, что при работе вызывать темплейт нужно из корневой папки проекта — это сделано потому что параметр applicationId проекта может вам наврать, если вы поменяли его в Gradle. А вот packageName так просто не проведешь, чем я и воспользовался. Доступным языком про шаблонизацию можно почитать по ссылкам ниже

Список литературы/медиа


1. Modern Android development: Android Jetpack, Kotlin, and more (Google I/O 2018, 40 m.) — краткий гайд на то, что сегодня в моде, отсюда также в общих чертах станет понятно как развивался RecyclerView;

2. Droidcon NYC 2016 — Radical RecyclerView, 36 m. — подробный доклад о RecyclerView от Lisa Wray;

3. Create a List with RecyclerView — официальная документация

4. Интерфейсы vs. классы

5. Android IDE Template Format, Тотальная шаблонизация, мануал FreeMarker — удобный подход, который в рамках этой статьи поможет быстро создавать нужные файлы по работе со списками

6. Код к статье(там немного другие названия классов, будьте внимательны), темплейты для работы и видео, как работать с темплейтами

7. Версия статьи на английском языке
Поделиться публикацией

Похожие публикации

Комментарии 12

    0

    Чего то я не понимаю, зачем в RV использовать адаптер, от которого бежали в привычном ListView?
    Кстати, а почему никто не пишет? У нас проект достаточно крупный и много где используется именно ListView, а переходить на RV — довольно долго и пока первого вполне хватает

      0
      Мне тоже кажется, что RV всего лишь ListView, вывернутый на изнанку.
      Те же адаптеры, те же вьюхолдеры и т.п.
      Видимо, его решили сделать, чтобы спрятать вьюхолдеры подальше, и сделать их принудительными. Чтобы не отвечать всегда на вопрос «Зачем он нужен?» фразой типа «Иначе всё тормозит». Чтобы глаза не мозолило.
        –1
        Да вот как-то честно не замечал особых тормозов при отрисовке ListView без ViewHolder'а. Конечно, данные я заранее готовлю и из массива беру объект со всеми подготовленными полями
          0
          Ну там самое медленное место это findViewById(). Но, мне кажется, если вью айтема простое, то и поиск будет быстрым :)
            0
            ну если там 200 TextView в каждом item'е, то конечно долго будет :)
      0

      Честно говоря, в первый же раз когда мне понадобилось несколько простых списков (тогда еще RV только только выходил) в приложении (и просто массивы и данные из sqlite) я подумал, что так просто нельзя писать, количество приседаний зашкаливало + огромные портянки кода, который не делает ничего полезного для бизнес логики. В итоге не совсем чисто, но загнал весь этот бойлерплейт в абстракные классы, фабрики и интрефейсы, после чего любой список добавлялся тривиально, он определяется адаптером, типом элементов и представлением элемента (для sqlite нужно присесть дополнительный раз), все проверяется на этапе компиляции. Если просто представить себе сколько написали люди всяких ViewHolder-ов, становится темно в глазах.

        0

        Спасибо за статью! Последовательно и разумно-подробно.


        Я как раз Kotlin изучаю, и планирую в частности что-нибудь для Android сделать.


        Методики решения задач всегда полезны — для формирования навыка самостоятельного решения.

          0
          Спасибо за статью, довольно интересный подход, но вот создавать список для настроек…
          Я такое встречал (до сих пор вздрагиваю, если честно), т.е. мы имеем статический лэйаут, который никогда не будет меняться (а только наполняться при создании), но пишем на него динамический компонент, с его анимацией(если потянуть список вниз, находясь в начале списка, например), обработкой каждого айтема как части списка (хотя там по сути просто вьюха для текста, вьюха для переключателя или текста и все) и вот этим вот всем?
          Хорошо, даже если его иногда надо менять, скажем обновить одно значение, для этого перерисовывать весь список или одну ячейку, а если асинхронно мы точно помним какую ячейку, а не повернет ли пользователь экран?
          В чем преимущество перед, скажем обычным вертикальным «LinearLayout» и addView() для каждого компонента, раз уж вы хотите динамическое его создание?
          На каждый экран нужен еще и адаптер с обработчиком по типу элемента, а не создаете ли вы этим проблему решая свою проблему?
          Скажем появится элемент «О пользователе» сверху, «Обратиться в поддержку» снизу, группировка по категориям, пара кастомных вьюх? А еще 5-8 экранов настроек (и это не прям чтобы редкость на самом деле) и для каждого будет сам экаран и класс адаптера…
            0
            Спасибо за отзыв.
            PreferenceFragment, стандартное решение от гуглов, точно также анимирован по свайпу — что в этом такого? Экран использован как пример, потому что часто встречаются элементы не просто с заголовками, а какие то из них интерактивны, требуют разных обработок — свитч, всплывающие окна, переходы на новые экраны. Все это обрабатывается в Rv, на мой взгляд, быстро и без проблем. Плюс, дает вам возможность быстро переиспользовать тот элемент, который вы реализовали. Датабайдинг упрощает эту возможность.
            Не волнуйтесь особо за перерисовку экрана. Устройства на нашем веку уже вполне могут быстро перерисовать список. По крайней мере для фрагмента с найстройками. =) А преждевременная оптимизация — это зло (с).
            Да собственно можно и LinearLayout использовать, но что если у вас количество элементов зайдет за рамки экрана? Будете ставить ScrollView? Rv обеспечит вам прокрутку, поэтому даже в стандартном PreferenceFragment она сразу же заложена. Насчет обработчиков по типу элементов — при инициализации Item вы можете указать разные callbacks, пример в этой статье на элементе со свитчем.
            Ответом на последний абзац будет — можно использовать DataBinding, чтобы уйти от разных адаптеров. Да это возможно и без него! В разделе ссылок есть доклад от Lisa Wray, там есть об этом. Посмотрите обязательно!
              0
              Не волнуйтесь особо за перерисовку экрана. Устройства на нашем веку уже вполне могут быстро перерисовать список. По крайней мере для фрагмента с найстройками. =) А преждевременная оптимизация — это зло (с).

              Похоже здесь мы с Вами из разных лагерей, и вот именно в этом примере, имхо, дело не в оптимизации, а раннем усложнении для дальнейшей расширяемости.

              Да собственно можно и LinearLayout использовать, но что если у вас количество элементов зайдет за рамки экрана? Будете ставить ScrollView?

              Именно, но это я тоже как пример привел и вот скролл конечно же закладывал бы изначально.

              Насчет обработчиков по типу элементов — при инициализации Item вы можете указать разные callbacks, пример в этой статье на элементе со свитчем.

              По поводу обработчиков по типу элемента — вам понадобится обработчик для каждого типа, и я это реализовывал именно LinearLayout-ом и именно «addView(view, clicklistener), addSwitch(switch, onCheckListener) и т.д.» при этом сделав такой относительно маленький абстрактный класс в 200 строк, я очень легко распространил его на остальные 23 экрана настроек… с Вашим подходом классов было бы в 2 раза больше, ну и я в каждом экране сразу вижу колбеки и вьюхи по мере добавления, а не иду в адаптер, но тут может у меня особый случай приложения.
              За датабайдинг спасибо, посмотрю…

              Дело не в том чтобы отказываться от свистелок в виде прокрутки и этой анимации в пользу «топорного» варианта, а в том чтобы использовать инструмент по назначению, RV очень красиво умеет прокручивать ленту с 4-5 типами элементов, добавлять с анимацией, удалять, сдвигать, при этом в горизонтальной и вертикальной ориентации списка, реализовать с его помощью настройки — именно микроскопом гвозди забивать (мое имхо конечно же)…

              А решение от гугла как раз заточено под то чтобы привязать сразу настройки к файлу по типу SharedPreferences, плюс реально добавить минимальный необходимый функционал ака категории и компоненты…
              «The fragment implementation itself simply populates the preferences when created. Note that the preferences framework takes care of loading the current values out of the app preferences and writing them when changed»

            0
            protected val items: ArrayList = ArrayList()
            protected val items: List = mutableListOf<>()
            Это же Kotlin

            override fun addAtPosition(pos: Int, newItem: IBaseListItem) {
            items.add(pos, newItem)
            notifyItemChanged(pos)
            }
            По мне data binding адское зло…
              0
              Можно всегда поправить мою реализацию в темплейте. ) Согласен, местами можно сделать более продумано. Если честно, у меня в разных проектах везде что-то меняется, то я открываю items, то делаю их приватными и доступными только по геттеру и сеттеру. protected тоже раньше ставил, тут в примере забил.
              Дело не в реализациях, а в общем подходе. Единая реализация важна, если у вас команда и есть какие то общие рекомендации и гайдстайлы.
              А почему data binding адское зло? Хотелось бы послушать развернутый ответ. Она много кому не нравится, но объяснить не могут позицию. Часто люди пробовали ее сырой, в то время как гуглы потихоньку делают ее лучше.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое