- PVSM.RU - https://www.pvsm.ru -
Примерно год назад я начал использовать Kotlin в своих Android проектах. Мне хотелось попробовать что-то новое, что было бы интересно изучать. Тогда я и наткнулся на Anko [1]. К тому времени писать UI на xml порядком осточертело. Мне всегда нравилось писать интерфейс руками, не прибегая к WYSIWYG и xml-разметке, используемой в Android Studio. Единственный минус заключается в том, что для проверки любого изменения придется перезапускать приложение. Можно использовать плагин [2], который показывает как будет выглядеть ui не запуская приложения, но мне он показался довольно странным. Так же у него есть крутая возможность конвертирования xml в Anko Layouts DSL.
Самый большой недостаток библиотеки — практически полное отсутствие документации. Чтобы разобраться, как ее правильно использовать, приходилось часто заглядывать в исходники. В этой статье будет подробно разобрано создание приложения используя Anko Layouts и Anko Coroutines.
Сама библиотека Anko разделяется на 4 независимые части:
Для добавления библиотеки в проект достаточно добавить одну строчку в зависимости проекта:
implementation "org.jetbrains.anko:anko:$anko_version"
где anko_version
— текущая версия библиотеки, прописанная в build.gradle файле на project уровне:
ext.anko_version='0.10.8'
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]. Разберем их поподробнее.
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.
Этот метод может быть очень полезен, однако в официальной документации на гихабе [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.
Таким образом у нас выполняется следующий алгоритм:
Более приближенный аналог: this.addView(AppFragmentUiToolbar().createView(...))
Как можно заметить, вариант с createDelegate более приятен для чтения.
Похоже на стандартный 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].
К счастью, 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.
Добавим свою реализацию для нашего 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
}
}
Для безопасной передачи чувствительных к утечкам памяти объектов используется метод 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
Нажмите здесь для печати.