- PVSM.RU - https://www.pvsm.ru -

Создание Android приложения с использованием Anko Layouts и Anko Coroutines

image

Примерно год назад я начал использовать Kotlin в своих Android проектах. Мне хотелось попробовать что-то новое, что было бы интересно изучать. Тогда я и наткнулся на Anko [1]. К тому времени писать UI на xml порядком осточертело. Мне всегда нравилось писать интерфейс руками, не прибегая к WYSIWYG и xml-разметке, используемой в Android Studio. Единственный минус заключается в том, что для проверки любого изменения придется перезапускать приложение. Можно использовать плагин [2], который показывает как будет выглядеть ui не запуская приложения, но мне он показался довольно странным. Так же у него есть крутая возможность конвертирования xml в Anko Layouts DSL.

Самый большой недостаток библиотеки — практически полное отсутствие документации. Чтобы разобраться, как ее правильно использовать, приходилось часто заглядывать в исходники. В этой статье будет подробно разобрано создание приложения используя Anko Layouts и Anko Coroutines.

Сама библиотека Anko разделяется на 4 независимые части:

  • Anko Layouts — построение UI.
  • Anko Commons — полезные инструменты и функции.
  • Anko SQLite — работа с базой данных SQLite.
  • Anko Coroutines — полезные инструменты для работы с корутинами.

Для добавления библиотеки в проект достаточно добавить одну строчку в зависимости проекта:

implementation "org.jetbrains.anko:anko:$anko_version"

где anko_version — текущая версия библиотеки, прописанная в build.gradle файле на project уровне:

ext.anko_version='0.10.8'

Anko Layouts

Anko Layouts позволяет разрабатывать UI Android приложения более эффективно, чем это было с использованием Java.

Основным игроком на поле у нас является интерфейс AnkoComponent<T> [3] с единственным методом createView, принимающим AnkoContext<T> [4] и возвращающим View. Как раз в этом методе и происходит создание всего UI. Интерфейс AnkoContext<T> [4] является оберткой над ViewManager [5]. Подробнее о нем будет позже.

Немного разобравшись с тем, как устроен AnkoComponent<T> [3], попробуем создать простенький UI в классе нашей Activity. Стоит уточнить, что вот такое "прямое" написание UI возможно только в Activity, так как для нее написана отдельная extension функция ankoView [6], в которой вызывается метод addView [7], и уже в самом методе создается AnkoContextImpl<T> [8] с параметром setContentView = true.

class AppActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        verticalLayout {
            lparams(matchParent, matchParent)
            gravity = Gravity.CENTER
            textView("Cool Anko TextView") {
                gravity = Gravity.CENTER
            }
        }
}

Очевидно, что для чего-то большего, чем один TextView, метод onCreate быстро превратится в подобие свалки. Попробуем отделить класс Activity от UI. Для этого создадим еще один класс, в котором он будет описываться.

class AppActivityUi: AnkoComponent<AppActivity> {
    override fun createView(ui: AnkoContext<AppActivity>): View = with(ui) {
        verticalLayout {
            lparams(matchParent, matchParent)
            gravity = Gravity.CENTER
            textView("Cool Anko TextView") {
                gravity = Gravity.CENTER
            }
        }
    }
}

Теперь, для того, что бы передать нашей Activity наш UI, можно использовать [9]

AppActivityUi().setContentView(this)

Хорошо, но как быть, если мы хотим создать UI для фрагмента? Для этого можно использовать метод createView напрямую, вызывая его из onCreateView метода фрагмента. Выглядит это следующим образом:

class AppFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return AppFragmentUi().createView(AnkoContext.create(requireContext(), this))
    }
}

Как уже было сказано — AnkoContext<T> [4] является оберткой над ViewManager. У его вспомогательного объекта (companion object) есть три основных метода, возвращающих AnkoContext<T> [4]. Разберем их поподробнее.

create

 fun <T> create(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>

и его брат-близнец

fun create(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>

возвращают AnkoContextImpl<T> [8].

Методы используются во всех стандартных случаях, как, например, в предыдущих примерах с Activity и Fragment. Самым интересным здесь являются параметры owner и setContentView. Первый позволяет передать в метод createView instance конкретного Fragment'a, Activity или чего-либо еще.

MyComponent().createView(AnkoContext.create(context, myVew))

class MyComponent: AnkoComponent<View> {
    override fun createView(ui: AnkoContext<View>): View = with(ui) {
        val myView: View= ui.owner
        // И дальше по инструкции
    }
}

Второй параметр — setContentView — автоматически попробует добавить полученный view, если Context является экземпляром Activity или ContextWrapper [10]. Если у него это не получится — он выкинет IllegalStateException.

createDelegate

Этот метод может быть очень полезен, однако в официальной документации на гихабе [11] о нем нигде не написано. Наткнулся я на него в исходниках, когда решал проблему раздутия UI классов:

fun <T: ViewGroup> createDelegate(owner: T): AnkoContext<T> = DelegatingAnkoContext(owner)

Он позволяет добавить результат createView компонента в owner.

Рассмотрим его использование на примере. Допустим, у нас есть большой класс, описывающий один из экранов приложения — AppFragmentUi.

verticalLayout {
    relativeLayout {
        id = R.id.toolbar
        // тут много вложенных view
    }
    relativeLayout{
        id = R.id.content
        // тут тоже много вложенных view
    }
}

Логически его можно разделить на две части — на тулбар и контент, AppFragmentUiToolbar и AppFragmentUiContent соответственно. Тогда наш основной класс AppFragmentUi станет гораздо проще:

class AppFragmentUi: AnkoComponent<AppFragment> {

    override fun createView(ui: AnkoContext<AppFragment>) = with(ui) {
        verticalLayout {
            AppFragmentUiToolbar().createView(AnkoContext.createDelegate(this))
            AppFragmentUiContent().createView(AnkoContext.createDelegate(this))
        }
    }
}

class AppFragmentUiToolbar : AnkoComponent<_LinearLayout> {
    override fun createView(ui: AnkoContext<_LinearLayout>): View = with(ui.owner) {
        relativeLayout {
            id = R.id.toolbar
            // тут вложенные view
        }
    }
}

Обратите внимание, что в функцию with [12] в качестве объекта передается не ui, a ui.owner.
Таким образом у нас выполняется следующий алгоритм:

  1. Создается инстанс компонента.
  2. Метод createView создает View который будет добавлен.
  3. Полученный View добавляется в owner.

Более приближенный аналог: this.addView(AppFragmentUiToolbar().createView(...))
Как можно заметить, вариант с createDelegate более приятен для чтения.

createReusable

Похоже на стандартный AnkoContext.create, но с небольшим дополнением — корневым view считается самый последний:

class MyComponent: AnkoComponent<MyObject> {
    override fun createView(ui: AnkoContext<MyObject>): View = with(ui) {
        textView("Some text")
        // в этом моменте не будет выкинут IllegalStateException: View is already set 
        // На экране будет показан "Another text"
        textView("Another text") 
    }
}

В стандартной реализации, если корневой view установлен — попытка установить второй view параллельно вызовет exception [13].

Метод createReusable возвращает класс ReusableAnkoContext [14], который наследуется от AnkoContextImpl [8] и переопределяет метод alreadyHasView() [15].

CustomView

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

verticalLayout {
    val view = CustomView(context)
    //....
    addView(view) // или addView(view.apply { ... })
}

Для этого можно добавлять свою обертку, которая будет делать тоже самое.

Основным компонентов здесь выступает extension метод <T: View>ankoView(factory: (Context) -> T, theme: Int, init: T.() -> Unit) [16] от ViewManager, Context или Activity.

  • factory — функция, на вход которой передаётся Context и возвращается View. По сути является фабрикой, в которой происходит создание View.
  • theme — ресурс стиля, который будет применен для текущей view.
  • init — функция, в которой для созданной View будут устанавливаться необходимые параметры.

Добавим свою реализацию для нашего CustomView

inline fun ViewManager.customView(theme: Int = 0, init: (CustomView).() -> Unit): CustomView {
    return ankoView({ CustomView(it) }, theme, init)
}

Теперь наш CustomView создается очень просто:

customView {
    id = R.id.customview
    // остальные параметры
}

Можно использовать lparams для применения LayoutParams к View.

textView("text") {
        textSize = 12f
    }.lparams(width = matchParent, height = wrapContent) {
        centerInParent()
}

Стоит заметить, что не ко всем View это применимо — все lparams методы как правило объявляются в обертках. Например _RelativeLayout [17] — обертка над RelativeLayout [18]. И так для каждого.

К счастью, для Android Support Library написано несколько оберток, поэтому можно только подключить зависимости в gradle файле.

    // Appcompat-v7 (Anko Layouts)
    implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
    implementation "org.jetbrains.anko:anko-coroutines:$anko_version"

    // CardView-v7
    implementation "org.jetbrains.anko:anko-cardview-v7:$anko_version"

    // Design
    implementation "org.jetbrains.anko:anko-design:$anko_version"
    implementation "org.jetbrains.anko:anko-design-coroutines:$anko_version"

    // GridLayout-v7
    implementation "org.jetbrains.anko:anko-gridlayout-v7:$anko_version"

    // Percent
    implementation "org.jetbrains.anko:anko-percent:$anko_version"

    // RecyclerView-v7
    implementation "org.jetbrains.anko:anko-recyclerview-v7:$anko_version"
    implementation "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version"

    // Support-v4 (Anko Layouts)
    implementation "org.jetbrains.anko:anko-support-v4:$anko_version"

    // ConstraintLayout
    implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"

Помимо всего прочего, библиотека позволяет более удобную имплементацию различных listener'ов. Небольшой пример из репозитория:

seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
    override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
    override fun onStopTrackingTouch(seekBar: SeekBar) = Unit
})

и теперь используя Anko

seekBar {
    onSeekBarChangeListener {
        onProgressChanged { seekBar, progress, fromUser ->
            // do something
        }
    }
}

Также некоторые listener'ы поддерживают корутины:

    verticalLayout{
    val anyCoroutineContext = GlobalScope.coroutineContext
    onClick(anyCoroutineContext) {
             //this: CoroutineScope
    }
}

Anko Coroutines

Для безопасной передачи чувствительных к утечкам памяти объектов используется метод asReference. Он базируется над WeakReference [19] и возвращает объект Ref. [20]

verticalLayout{
    val activity = ui.owner
    val activityReference: Ref<AppActivity> = activity.asReference()
    onClick(anyCoroutineContext) {
        ref().doSomething()
    }
}

Допустим, требуется в стандартный ViewPager.OnPageChangeListener [21] добавить поддержку корутин. Сделаем его таким же крутым, как и пример с seekbar'ом.
Во-первых, создаем отдельный класс и наследуемся от ViewPager.OnPageChangeListener [21].

class CoroutineOnPageChangeListener(
    private val coroutineContext: CoroutineContext = Dispatchers.Main
) : ViewPager.OnPageChangeListener {

}

В переменных будем хранить лямбды, которые будут вызываться ViewPager.OnPageChangeListener [21].

    private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null
    private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null
    private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null

Реализуем инициализацию для одной из этих переменных (остальные делаются аналогично)

    fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) {
        onPageScrollStateChanged = action
    }

И в конце имплементируем функцию с таким же названием.

    override fun onPageScrollStateChanged(state: Int) {
        GlobalScope.launch(coroutineContext) {
            onPageScrollStateChanged?.invoke(state, coroutineContext)
        }
    }

Осталось добавить extension функцию, чтобы это все работало

fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) {
    val listener = CoroutineOnPageChangerListener()
    listener.init()
    addOnPageChangeListener(listener)
}

И вставляем всё это дело под ViewPager

viewPager {
    onPageChangeListenerCoroutines {
        onPageScrolled { position, offset, pixels, coroutineContext ->
            // делаем что-нибудь полезное в корутине.
        }
    }
}

Полный код тут

class CoroutineOnPageChangeListener(
    private val coroutineContext: CoroutineContext = Dispatchers.Main
) : ViewPager.OnPageChangeListener {

    private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null
    private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null
    private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null

    fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) {
        onPageScrollStateChanged = action
    }

    fun onPageScrolled(action: ((Int, Float, Int, CoroutineContext) -> Unit)?) {
        onPageScrolled = action
    }

    fun onPageSelected(action: ((Int, CoroutineContext) -> Unit)?) {
        onPageSelected = action
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        GlobalScope.launch(coroutineContext) {
            onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels, coroutineContext)
        }
    }

    override fun onPageSelected(position: Int) {
        GlobalScope.launch(coroutineContext) {
            onPageSelected?.invoke(position, coroutineContext)
        }
    }

    override fun onPageScrollStateChanged(state: Int) {
        GlobalScope.launch(coroutineContext) {
            onPageScrollStateChanged?.invoke(state, coroutineContext)
        }
    }
}

fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) {
    val listener = CoroutineOnPageChangerListener()
    listener.init()
    addOnPageChangeListener(listener)
}

Так же в библиотеке Anko Layouts есть множество полезных методов, таких как переводы в различные метрики [22].

Автор: Makentoshe

Источник [23]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/310435

Ссылки в тексте:

[1] Anko: https://github.com/Kotlin/anko

[2] плагин: https://plugins.jetbrains.com/plugin/7734-anko-support

[3] AnkoComponent<T>: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L131

[4] AnkoContext<T>: https://github.com/Kotlin/anko/blob/master/anko/library/static/commons/src/main/java/AnkoContext.kt

[5] ViewManager: https://developer.android.com/reference/android/view/ViewManager"

[6] ankoView: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/Custom.kt#L43

[7] addView: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/Internals.kt#L62

[8] AnkoContextImpl<T>: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L86

[9] можно использовать: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L135

[10] является экземпляром Activity или ContextWrapper: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L110

[11] официальной документации на гихабе: https://github.com/Kotlin/anko/wiki/Anko-Layouts

[12] with: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.html

[13] попытка установить второй view параллельно вызовет exception: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L100

[14] ReusableAnkoContext: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L78

[15] alreadyHasView(): https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/static/commons/src/main/java/AnkoContext.kt#L83

[16] <T: View>ankoView(factory: (Context) -> T, theme: Int, init: T.() -> Unit): https://github.com/Kotlin/anko/blob/master/anko/library/static/commons/src/main/java/Custom.kt

[17] _RelativeLayout: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/generated/sdk28/src/main/java/Layouts.kt#L1140

[18] RelativeLayout: https://developer.android.com/guide/topics/ui/layout/relative

[19] WeakReference: https://developer.android.com/reference/java/lang/ref/WeakReference

[20] Ref.: https://github.com/Kotlin/anko/blob/acb7e606dfca0ebd5becf04b93c5b932e9575246/anko/library/generated/coroutines/src/main/java/weakReferenceSupport.kt#L24

[21] ViewPager.OnPageChangeListener: https://developer.android.com/reference/android/support/v4/view/ViewPager.OnPageChangeListener

[22] различные метрики: https://github.com/Kotlin/anko/blob/master/anko/library/static/commons/src/main/java/Dimensions.kt

[23] Источник: https://habr.com/ru/post/442440/?utm_source=habrahabr&utm_medium=rss&utm_campaign=442440