Всем привет! Мои посты - это желание помочь в работе с некоторыми элементами Android. Если вы разработчик, который еще не создал алгоритм построения списков, возможно, вам будет полезно прочитать этот материал.
По сути, я хотел бы предложить готовые решения для развития, раскрывая по ходу рассказа некоторые мысли о том, как я к ним пришел и к этому пришел.
В этой статье:
- создаем несколько базовых классов и интерфейсов для работы с RecyclerView и RecyclerView.Adapter
- подключим одну библиотеку от Android Jetpack (необязательно, сначала без нее)
- для еще более быстрой разработки — вариант шаблона в конце статьи ;)
Введение
Хорошо! Про ListView уже все забыли и радостно пишут в RecyclerView( Рв ).Те времена, когда мы сами реализовывали паттерн ПросмотрДержатель , канули в Лету.
Рв предоставляет нам набор готовых классов для реализации списков и достаточно большой выбор Менеджеры макетов чтобы отобразить их.
На самом деле, глядя на множество экранов, большинство из них можно представить в виде списка — именно благодаря способности каждого элемента реализовывать свои собственные.
ПросмотрДержатель .
Более подробная история развития нам сказали на Google I/O .
Но, всегда есть парочка «но»!.
Стандартные ответы на Stackoverflow предполагают общие решения, приводящие к копипасту, особенно там, где реализован Адаптер.
На данный момент, Рв уже три года.
Информации по нему масса, и библиотек с готовыми решениями много, но что делать, если весь функционал вам не нужен, или вы идете смотреть чужой код — и видите Древний Ужас там не такой, какой вам хотелось бы видеть, или совсем не так вы себе представляли? За эти три года Android наконец-то официально принял Kotlin = читаемость кода улучшилась, по словам Рв Опубликовано множество интересных статей, полностью раскрывающих его возможности.
Цель этого — собрать основу из лучших практик для вашего велосипеда, фреймворк для работы со списками для новых приложений.
Эту структуру можно расширять с помощью логики от приложения к приложению, используя то, что вам нужно, и отбрасывая то, что вам не нужно.
Я считаю, что такой подход гораздо лучше чужой библиотеки — на занятиях у вас есть возможность разобраться, как все работает, и контролировать нужные вам кейсы, не привязываясь к чужому решению.
Давайте подумаем логически и с самого начала
Решение о том, что должен делать компонент, будет интерфейс, а не класс , но в конце мы закроем конкретную логику реализации класса, который будет реализовывать и реализовывать этот интерфейс.Но, если окажется, что при реализации интерфейса создается копипаста, мы можем спрятать его за абстрактным классом, а после него — классом, наследуемым от абстрактного.
Я покажу свою реализацию базовых интерфейсов, но моя цель — чтобы разработчик просто попробовал думать в том же направлении.
Еще раз план такой: Набор интерфейсов -> абстрактный класс , берём копипаст (если надо) -> и уже класс бетона с уникальным кодом .
Вы можете реализовать интерфейсы по-разному.
Что адаптер может делать со списком? Ответ на этот вопрос легче всего получить, рассмотрев пример.
Можете заглянуть в RecyclerView.Adapter, там найдете пару советов.
Если немного подумать, то можно представить себе примерно следующие способы: Адаптер IBaseListAdapter
* Просматривая проекты, я нашел еще несколько методов, которые здесь опущу, например getItemByPos (позиция: Int), или даже подсписок (startIndex: Int, endIndex: Int).interface IBaseListAdapter<T> { fun add(newItem: T) fun add(newItems: ArrayList<T>?) fun addAtPosition(pos : Int, newItem : T) fun remove(position: Int) fun clearAll() }
Повторюсь: вы сами должны посмотреть, что вам нужно от проекта, и включить функции в интерфейс.
Это не сложно, когда знаешь, что все происходит на одном занятии.
Аскетизм в этом вопросе позволит избавиться от лишней логики, которая ухудшает читабельность кода, ведь конкретная реализация занимает больше строк.
Обратите внимание на общий Т .
В общем, адаптер работает с любым объектом списка (пунктом), поэтому здесь нет никаких уточнений — мы еще не определились со своим подходом.
И в этой статье их будет как минимум два, первый интерфейс выглядит так: 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() (следующая строка).
Создание списка
В качестве примера возьмем экран настроек.Андроид предлагает нам свой вариант, а что, если конструкция требует чего-то более изощренного? Я предпочитаю заполнять этот экран в виде списка.
Вот такой случай:
Мы видим два разных элемента списка, что означает 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 - очень широкая тема, хотелось бы поговорить о ней подробнее в других статьях, но сейчас мы будем использовать ее только в рамках текущей - сделаем свою Арт.кт — переменная Для элемент.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
-
«Уши» На Любой Вкус
19 Oct, 24 -
Ошибка В Карме.
19 Oct, 24 -
Карма - Сколько Голосов У Меня Осталось?
19 Oct, 24