Разработка Для Android. Немного О Быстрой Работе Со Списками

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

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

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

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


Введение

Хорошо! Про ListView уже все забыли и радостно пишут в RecyclerView( Рв ).

Те времена, когда мы сами реализовывали паттерн ПросмотрДержатель , канули в Лету.

Рв предоставляет нам набор готовых классов для реализации списков и достаточно большой выбор Менеджеры макетов чтобы отобразить их.

На самом деле, глядя на множество экранов, большинство из них можно представить в виде списка — именно благодаря способности каждого элемента реализовывать свои собственные.

ПросмотрДержатель .

Более подробная история развития нам сказали на Google I/O .

Но, всегда есть парочка «но»!.

Стандартные ответы на Stackoverflow предполагают общие решения, приводящие к копипасту, особенно там, где реализован Адаптер.

На данный момент, Рв уже три года.

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

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

Эту структуру можно расширять с помощью логики от приложения к приложению, используя то, что вам нужно, и отбрасывая то, что вам не нужно.

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



Давайте подумаем логически и с самого начала

Решение о том, что должен делать компонент, будет интерфейс, а не класс , но в конце мы закроем конкретную логику реализации класса, который будет реализовывать и реализовывать этот интерфейс.

Но, если окажется, что при реализации интерфейса создается копипаста, мы можем спрятать его за абстрактным классом, а после него — классом, наследуемым от абстрактного.

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

Еще раз план такой: Набор интерфейсов -> абстрактный класс , берём копипаст (если надо) -> и уже класс бетона с уникальным кодом .

Вы можете реализовать интерфейсы по-разному.

Что адаптер может делать со списком? Ответ на этот вопрос легче всего получить, рассмотрев пример.

Можете заглянуть в 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 (позиция: Int), или даже подсписок (startIndex: Int, endIndex: Int).

Повторюсь: вы сами должны посмотреть, что вам нужно от проекта, и включить функции в интерфейс.

Это не сложно, когда знаешь, что все происходит на одном занятии.

Аскетизм в этом вопросе позволит избавиться от лишней логики, которая ухудшает читабельность кода, ведь конкретная реализация занимает больше строк.

Обратите внимание на общий Т .

В общем, адаптер работает с любым объектом списка (пунктом), поэтому здесь нет никаких уточнений — мы еще не определились со своим подходом.

И в этой статье их будет как минимум два, первый интерфейс выглядит так:

interface IBaseListItem { fun getLayoutId(): Int }

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

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

Пришло время объединить наши Адаптер 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 (позиция: Int) .

Нам нужен какой-то интеллектуальный ключ, по которому Rv поймет, какой ViewHolder нам подходит. Отлично подходит для этого Val макетид наш элемент , потому что Android каждый раз услужливо делает идентификаторы макетов уникальными, и все значения больше нуля — этим мы будем пользоваться дальше, «раздувая» элементView для наших зрителей в методе раздуватьByViewType() (следующая строка).



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

В качестве примера возьмем экран настроек.

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

Вот такой случай:

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

Мы видим два разных элемента списка, что означает SimpleListAdapter И Рв здесь было бы идеально! Давайте начнем! Начать можно с макетов размещения предметов: 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>

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

Чтобы отличить элементы Switch, подойдут идентификаторы сущностей с сервера; если их нет, мы можем создать их сами во время инициализации.

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: 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 (контекст, viewType, родитель): viewType = идентификатор макета.

Все компоненты готовы! Теперь код активности остается, и вы можете запустить программу: 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>

НастройкиActivity.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. Возьмите их титулы .

Я использую правило: Что-нибудь Item.kt, item_ что-нибудь .

xml, Что-нибудь ViewHolder.kt 3. Напишите в эти классы адаптер .

В принципе, если не претендовать на оптимизацию, то одного обычного переходника будет достаточно.

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

onBindViewHolder() (страдает читаемость кода) в вашем адаптере (в нашем случае это НастройкиСписокАдаптер ) + программе придется проходить этот метод каждый раз, для каждого пункта + по методу onCreateViewHolder() 4. Запускайте код и радуйтесь!

ДжетПак

До этого момента мы использовали стандартный подход к привязке данных из Арт.кт - нашим item_layout.xml .

Но мы можем унифицировать метод onBindViewHolder() , оставьте минимальным и перенесите логику в Item и Layout. Перейдем на официальную страницу Android JetPack:

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

Обратим внимание на первую вкладку в разделе Архитектура.

Привязка данных Android - очень широкая тема, хотелось бы поговорить о ней подробнее в других статьях, но сейчас мы будем использовать ее только в рамках текущей - сделаем свою Арт.кт переменная Для элемент.xml (или вы можете назвать это моделью представления макета).

На момент написания статьи Привязка данных вы можете подключиться так:

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)

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

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

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

Наши товары: 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 — теперь любой наш макет поддерживается собственной моделью представления, и он спокойно будет обрабатывать всю логику кликов, анимаций, запросов и прочего.

Что бы вы ни придумали.

Это очень поможет Привязка адаптеров - позволяющий связать представление с данными любого типа.

Кроме того, общение можно улучшить благодаря двусторонняя привязка данных .

Вероятно, оно появится в одной из следующих статей; в этом примере все можно сделать проще.

Нам нужен только один адаптер привязки:

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

После этого мы связываем значения наших переменных с нашими Элемент внутри 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}" - в этой линии мы использовали наш Адаптер привязки *Примечание: По справедливым причинам некоторым может показаться, что мы пишем гораздо больше кода в xml — но это вопрос знания библиотеки привязки данных Android. Он дополняет макет, быстро читается и, в принципе, по большей части убирает шаблонность.

Я думаю, что Google собирается хорошо развивать эту библиотеку, поскольку она первая на вкладке «Архитектура» в Android Jetpack. Попробуйте поменять MVP на MVVM в паре проектов — и многие могут быть приятно удивлены.

Ну тогда!.

А, код в SettingsActivity: НастройкиActivity.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() } } }



Нижняя граница

Мы получили алгоритм построения списков и инструменты для работы с ними.

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

xml и последующей привязке к переменным в .

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

Обратите внимание, что при работе шаблон нужно вызывать из корневой папки проекта - это делается потому, что параметр идентификатор приложения project может вам лгать, если вы изменили его в Gradle. И здесь имя пакета Сделать это так просто не получится, чем я и воспользовался.

О шаблонизации доступным языком вы можете прочитать по ссылкам ниже.



Список литературы/СМИ

1. Современная Android-разработка: Android Jetpack, Kotlin и многое другое (Google I/O 2018, 40 мин.

) — краткий путеводитель по тому, что сегодня в моде, отсюда также в общих чертах станет понятно, как развивался RecyclerView; 2. Droidcon NYC 2016 — Radical RecyclerView, 36 ф.

— подробный отчет по RecyclerView от Лиза Рэй ; 3. Создайте список с помощью RecyclerView - официальная документация 4. Интерфейсы против классов 5. Формат шаблона Android IDE , Тотальное шаблонирование , Руководство по FreeMarker — удобный подход, который в рамках данной статьи поможет быстро создать необходимые файлы для работы со списками.

6. Код для статьи (названия классов немного другие, будьте осторожны), шаблоны для работы И видео о том, как работать с шаблонами 7. Версия статьи на английский язык Теги: #Kotlin #Android #template #templates #recyclerview #adapter #jetpack #jetpack sdk #привязка данных #list #lists #Разработка мобильных приложений #дизайн и рефакторинг #Разработка Android #Kotlin

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.