DataBinding on Anko
はじめに
ホットペッパービューティーでAndroid開発を担当している@sakuna63です。
この記事ではAnkoというライブラリの上でデータバインディング機構を実現する方法について考えてみたいと思います。
Anko
AnkoはKotlinで作られたJetBrains製のAndroidライブラリです。Commons, Layouts, SQLite, Coroutinesの4つのコンポーネントから構成されており、それぞれがKotlinの言語機能を活用した拡張関数やモジュールを提供しています。
このうちLayoutsはレイアウトを構築するためのDSLを提供するコンポーネントです。以下のようにして、レイアウトを構築することができます。
frameLayout {
button {
text = "Greeting"
onClick {
toast(edit.text)
}
}.lparams { gravity = Gravity.CENTER }
}
AndroidのレイアウトXMLを書いたことがある人なら、何をしているのかなんとなく理解できると思います。特徴的なのは、レイアウト定義の中でクリックリスナを設定している点です。DSL自体がKotlinなので、このような記述が可能となっています。同様に、DSLの独自拡張もKotlinの言語機能(拡張関数, 中置関数 etc..)を使って容易に行うことができます。
今回はこのAnko LayoutsのDSLを拡張することで、データバインディング機構を実現してみようと思います。
実装
データバインディングに必要な要素
データバインディング機構を実現するのに必要な要素は以下の3つだと考えています。
- ViewModelのプロパティ変更監視
- Viewのプロパティの変更監視
- ViewModelのプロパティと、Viewのプロパティのマッピング
今回は3.
をAnkoのDSLを拡張して実装してみようと思います。 1.
の実現にはData Binding LibraryのObservable
をそのまま流用します。2.
はViewが提供しているリスナを利用し、提供されていないものについては考えません。
プロパティのマッピング
先程のサンプルにもありましたが、Ankoでは基本的に=
を使ってViewのプロパティへ値を代入します。
editText {
hint = "This is Hint"
}
ちなみにhint =
はJavaのsetHint
を呼び出しているだけであり、Ankoの提供している機能ではありません。
Data Binding Libraryに習って、マッピングも hint = viewModel.hint
のように記述したいところです。試しに以下のような拡張プロパティを実装してみます。
var TextView.hint: ObservableField<CharSequence>
@Deprecated(AnkoInternals.NO_GETTER, level = DeprecationLevel.ERROR) get() = AnkoInternals.noGetter()
set(value) {
hint = value.get()
value.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(p0: Observable?, p1: Int) {
hint = value.get()
}
})
}
しかし、これを使うことはできません。なぜかというと hint = viewModel.hint
と記述した場合、TextView#setHint
の呼び出しと推論され、型のミスマッチとして扱われるためです。また、仮に可能だったとしても、Viewのプロパティ一つ一つに対してこのような実装を行うのは厳しいものがあります。
前者については自然さを捨てて違う構文を提供すれば解決できそうです、後者は関数リファレンスを活用することでパターン化できそうな気配があります。
結論として、以下のような関数を実装することで解決しました。DSLとしての自然さは損なわれてしまいますが、汎用的に記述できるため個人的には満足しています。
editText {
bind(::setHint, viewModel.hint)
}
fun <T : Any, U : T> bind(setter: (T) -> Unit, field: ObservableField<U>) {
setter(field.get())
field.addOnPropertyChangedCallback { setter(it) }
}
双方向バインディング
双方向バインディングについては汎用的に記述することはできませんでした。そもそもViewの状態を監視するにはViewが提供しているListenerを利用するしかなく、それぞれでインターフェースは異なります。リフレクションを使えば実現できるかもしれませんがそれは避けたいところです。これについてはData Binding Libraryも同じ悩みを抱えています。
とはいえ双方向バインディングが必要となるケースは単方向バインディングに比べれば圧倒的に少ないはずです。なので双方向バインディングは一つ一つ実装することにしました。例えば、TextView#text
への双方向バインディングを以下のように実装しています。
editText {
textBind = viewModel.text
}
fun TextView.bindText(field: ObservableField<String>) {
bind(::setText, field)
textChangedListener {
afterTextChanged {
// prevent infinite loop
if (field.get() == it.toString()) {
return@afterTextChanged
}
field.set(it.toString())
}
}
}
サンプルコード
上記手法を模索するにあたり、googlesamples/android-architecture
のtodo-mvvm-databinding
を一部Kotlinize && Anko化してみました。もし興味がありましたら御覧ください。
終わりに
今回はAnkoを使ってデータバインディング機構を実装する方法について紹介しました。
本家Data Binding Libraryに比べると、Kotlinの言語機能に閉じた拡張であり、黒魔術感が少ないなと感じました。Ankoが今後どう発展していくのかが気がかりではありますが、次の機会があれば是非候補として検討したいところです。
Author And Source
この問題について(DataBinding on Anko), 我々は、より多くの情報をここで見つけました https://qiita.com/sakuna63/items/061c646ce49055f9ab1b著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .