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

в 14:12, , рубрики: android, kotlin, Разработка под android

image

Примерно год назад я начал использовать Kotlin в своих Android проектах. Мне хотелось попробовать что-то новое, что было бы интересно изучать. Тогда я и наткнулся на Anko. К тому времени писать UI на xml порядком осточертело. Мне всегда нравилось писать интерфейс руками, не прибегая к WYSIWYG и xml-разметке, используемой в Android Studio. Единственный минус заключается в том, что для проверки любого изменения придется перезапускать приложение. Можно использовать плагин, который показывает как будет выглядеть 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> с единственным методом createView, принимающим AnkoContext<T> и возвращающим View. Как раз в этом методе и происходит создание всего UI. Интерфейс AnkoContext<T> является оберткой над ViewManager. Подробнее о нем будет позже.

Немного разобравшись с тем, как устроен AnkoComponent<T>, попробуем создать простенький UI в классе нашей Activity. Стоит уточнить, что вот такое "прямое" написание UI возможно только в Activity, так как для нее написана отдельная extension функция ankoView, в которой вызывается метод addView, и уже в самом методе создается AnkoContextImpl<T> с параметром 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, можно использовать

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> является оберткой над ViewManager. У его вспомогательного объекта (companion object) есть три основных метода, возвращающих AnkoContext<T>. Разберем их поподробнее.

create

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

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

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

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

Методы используются во всех стандартных случаях, как, например, в предыдущих примерах с 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. Если у него это не получится — он выкинет IllegalStateException.

createDelegate

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

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

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) от 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 — обертка над RelativeLayout. И так для каждого.

К счастью, для 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 и возвращает объект Ref.

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

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

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

}

В переменных будем хранить лямбды, которые будут вызываться 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
    }

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

    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 есть множество полезных методов, таких как переводы в различные метрики.

Автор: Makentoshe

Источник

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