Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 2)

в 9:22, , рубрики: android, kotlin, библиотека, Разработка под android

Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 2) - 1
Пришло время поговорить об обещанных «квантовых» частицах. Kuantum позволяет реактивно изменять состояния UI. На создание этой библиотеки меня вдохновил Vue. Сразу покажу библиотеку в бою.

Практика

Часть-1: «Квантовая» связка частиц
Продолжаем модифицировать приложение из предыдущего поста. Теперь добавим возможность добавлять/удалять элементы словаря и изменять их значения.

В build.gradle (app level) добавьте следующие строки
implementation 'com.github.Brotandos:koatl:v0.1.3'
implementation 'com.github.Brotandos:kuantum:v0.0.7'

Меняем Models.kt

import com.brotandos.kuantumlib.ListKuantum
import com.brotandos.kuantumlib.TextKuantum

// Маркер 'q' означает "квантовую" переменную
data class Dictionary(var qTitle: TextKuantum, val qItems: ListKuantum<DictionaryItem>)
data class DictionaryItem(val qKey: TextKuantum, val qValue: TextKuantum)

Самое веселое, DictionaryFragment.kt:


import android.graphics.Color
import android.view.View
import android.widget.Button
import android.widget.ImageView
import com.brotandos.koatlib.*
import com.brotandos.kuantumlib.ListKuantum
import com.brotandos.kuantumlib.TextKuantum
import com.brotandos.kuantumlib.of
import org.jetbrains.anko.imageResource
import org.jetbrains.anko.matchParent
import org.jetbrains.anko.scrollView

class DictionaryFragment: KoatlFragment(), View.OnClickListener {
    // Маркер 'q' означает, что это "квантовая" переменная
    private val qDictionary: Dictionary
    private val icCollapsed = R.drawable.ic_collapsed
    private val icExpanded = R.drawable.ic_expanded

    init {
        val list = mutableListOf<DictionaryItem>()
        for (i in 0 until 7) list += DictionaryItem(TextKuantum("key-$i"), TextKuantum("value-$i"))

        qDictionary = Dictionary(TextKuantum("First dictionary"), ListKuantum(list))
    }

    override fun markup() = KUI {
        scrollView { vVertical {
            vFrame(bg(R.color.colorPrimary)) {
                // Слово "of" - infix функция.
                // Благодаря ней view-частица привязывается к тексту 
                vLabel(10f.sp, text(Color.WHITE)).lp(submissive, g5) of qDictionary.qTitle
            }.lparams(matchParent, 50.dp)
            // Нижнюю частицу я вставил для демонстрации силы TextKuantum
            // Здесь мы привязываем титул словаря. Попробуйте туда что-нибудь написать
            vText(line, "Set dictionary name"()).lp(row) of qDictionary.qTitle
            // Функция String.invoke прописывает placeholder-текст для EditText

            // Привязываем recyclerView к списку и описываем верстку для каждого элемента
            vList(linear).lp(row, m(5.dp)) of qDictionary.qItems.vForEach { item, _ ->
                vVertical(bgLayerCard, mw) {
                    vLinear(content456) {
                        // Кнопка свернуть/развернуть
                        vImage(icCollapsed, tag(item), this@DictionaryFragment()).lp(row, 5f())
                        // Привязываем само слово
                        vText(line).lp(row, 1f()) of item.qKey
                        // Кнопка удаления слова
                        vImage(R.drawable.ic_remove, tag(item), this@DictionaryFragment()).lp(row, 5f())
                    }.lp(dominant, 1f())
                    // Привязываем значение слова
                    vText(text(G.Color.PRIMARY), hidden).lp(dominant, 1f(), m(2.dp)) of item.qValue
                }.llp(row, m(2.dp))
            }
            vBtn("Add item to qDictionary", this@DictionaryFragment())
        }}
    }

    override fun onClick(v: View?) {
        // Нижняя функция самая сложная для понимания.
        // Мы внутри списка ищем подходящий элемент словаря...
        // ... и возвращаем view-частицу для описания слова
        // Оно нужно, чтобы развернуть элемент словаря...
        // ... и показать view, отвечающую за описание слова
        fun ListKuantum<DictionaryItem>.getItemValueView(itemAsViewTag: Any)
        = find { it == itemAsViewTag }!!.qValue.firstView

        if (v is Button) {
            // Добавляем в словарь новый элемент
            qDictionary.qItems.add(DictionaryItem(TextKuantum(), TextKuantum()))
        }
        else when ((v as ImageView).resourceId) {
            // Разворачиваем слово
            icCollapsed -> {
                v.imageResource = icExpanded
                qDictionary.qItems.getItemValueView(v.tag).visible()
            }
            // Сворачиваем слово
            icExpanded  -> {
                v.imageResource = icCollapsed
                qDictionary.qItems.getItemValueView(v.tag).hidden()
            }
            // Удаляем элемент
            R.drawable.ic_remove -> qDictionary.qItems.removeFirstWhere { it == v.tag }
        }
    }
}

Результат

Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 2) - 2

Код на github

Часть-2: Фильтр и BooleanKuantum
Теперь добавим возможность поиска по словарю.

Добавим иконку поиска

Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 2) - 3

Изменим DictionaryFragment.kt (места изменений я пометил комментариями)


import android.graphics.Color
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.Button
import android.widget.ImageView
import com.brotandos.koatlib.*
import com.brotandos.kuantumlib.*
import org.jetbrains.anko.imageResource
import org.jetbrains.anko.matchParent
import org.jetbrains.anko.scrollView

class DictionaryFragment: KoatlFragment(), View.OnClickListener {
    private val icCollapsed = R.drawable.ic_collapsed
    private val icExpanded = R.drawable.ic_expanded
    private val qDictionary: Dictionary
    // Текст поиска
    private val qFilter = TextKuantum()
    // Флаг поиска по ключу
    private val qFilterByKey = BooleanKuantum(true)
    // Флаг поиска по значению
    private val qFilterByValue = BooleanKuantum(true)
    // Сохраним view-частицу, нам она пригодится
    private lateinit var vList: RecyclerView

    init {
        val list = mutableListOf<DictionaryItem>()
        for (i in 0 until 7) list += DictionaryItem(TextKuantum("key-$i"), TextKuantum("value-$i"))

        qDictionary = Dictionary(TextKuantum("First dictionary"), ListKuantum(list))
    }

    override fun markup() = KUI {
        scrollView { vVertical {
            vFrame(bg(R.color.colorPrimary)) {
                vLabel(10f.sp, text(Color.WHITE)).lp(submissive, g5) of qDictionary.qTitle
            }.lparams(matchParent, 50.dp)
            // Строка поиска
            vText(line, "Search"(), icLeft(R.drawable.ic_search), pIcon(3.dp)).lp(row) of qFilter {
                // Здесь прописывается реакция на изменение текста поиска
                if (it.isEmpty()) {
                    qDictionary.qItems.clearViewFilter()
                    vList.scrollToPosition(0)
                }
                else filterDictionary(it)
            }
            vLinear(p(2.dp)) {
                // Флаг поиска по ключу
                vCheck("by key").lp(row, 1f()) of qFilterByKey {
                    vList.requestLayout()
                    if (it.not() && qFilterByValue.value.not()) {
                        qFilterByKey becomes true
                        qFilterByValue becomes true
                    }
                    filterDictionary(qFilter.value)
                }
                // Флаг поиска по значению
                vCheck("by value").lp(row, 1f()) of qFilterByValue {
                    vList.requestLayout()
                    if (it.not() && qFilterByKey.value.not()) {
                        qFilterByKey becomes true
                        qFilterByValue becomes true
                    }
                    filterDictionary(qFilter.value)
                }
            }.lp(submissive, g258)
            vList = vList(linear).lp(row, m(5.dp)) of qDictionary.qItems.vForEach { item, _ ->
                vVertical(bgLayerCard, mw) {
                    vLinear(content456) {
                        vImage(icCollapsed, tag(item), this@DictionaryFragment()).lp(row, 5f())
                        vText(line).lp(row, 1f()) of item.qKey
                        vImage(R.drawable.ic_remove, tag(item), this@DictionaryFragment()).lp(row, 5f())
                    }.lp(dominant, 1f())
                    vText(text(G.Color.PRIMARY), hidden).lp(dominant, 1f(), m(2.dp)) of item.qValue
                }.llp(row, m(2.dp))
            }
            vBtn("Add item to dictionary", this@DictionaryFragment()).lp(row)
        }}
    }

    override fun onClick(v: View?) {
        fun ListKuantum<DictionaryItem>.getItemValueView(itemAsViewTag: Any)
        = find { it == itemAsViewTag }!!.qValue.firstView

        if (v is Button) {
            qDictionary.qItems.add(DictionaryItem(TextKuantum(), TextKuantum()))
            vList.scrollToPosition(qDictionary.qItems.lastIndex)
        }
        else when ((v as ImageView).resourceId) {
            icCollapsed -> {
                v.imageResource = icExpanded
                qDictionary.qItems.getItemValueView(v.tag).visible()
            }
            icExpanded  -> {
                v.imageResource = icCollapsed
                qDictionary.qItems.getItemValueView(v.tag).hidden()
            }
            R.drawable.ic_remove -> qDictionary.qItems.removeFirstWhere { it == v.tag }
        }
    }

    // Фильтруем словарь
    private fun filterDictionary(filterWord: String) {
        qDictionary.qItems.filterView {
            if (qFilterByKey.value && qFilterByValue.value.not()) it.qKey.value.contains(filterWord)
            else if (qFilterByValue.value && qFilterByKey.value.not()) it.qValue.value.contains(filterWord)
            else it.qKey.value.contains(filterWord) || it.qValue.value.contains(filterWord)
        }
    }
}

Результат

Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 2) - 4

Код коммита на github

Теперь у нас готов фрагмент словаря с фильтром. Все вместилось меньше чем 100 строк. Без никаких xml, адаптеров, findViewById и танцев с textWatcher. И это все в одном файле, не нужно прыгать между версткой и логикой.

В следующий раз покажу прогрузку с интернета и подведу итоги, распишу известные мне плюсы и минусы, какие риски вас ждут.

Автор: Бауыржан

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js