AnkoComponent実装クラスのプロパティ宣言をなんとかしたい
AnkoComponent実装クラスをViewHolderのように使用する時には、外から使用したいViewをプロパティとして宣言しています。これをKotlinらしいコードにするために一工夫してみます。
NotNullにしたい
以下のようなコードがあったとします。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ui = MainActivityUi()
ui.setContentView(this)
ui.label?.text = "Hello, Anko!" // nullチェックが入る
}
}
class MainActivityUi() : AnkoComponent<MainActivity> {
val textViewId = View.generateViewId()
var label : TextView? = null // null許容型
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
label = textView {
}
}
}
}
MainActivityUiのlabelがnull許容型のため、使用側で ui.label?.text
というようにnullチェックをする必要が生じています。
nullチェックを不要にするにはどうしたらいいでしょうか?
lateinit修飾子を使用すればNotNullにすることができます。
lateinit修飾子を付加すると、プロパティの使用時まで初期化を遅延させることができます。
プロパティが使用されるまでにnull以外の値が入ることが確実な場合に使用します。
lateinit修飾子を使うと以下のようにできます。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ui = MainActivityUi()
ui.setContentView(this)
ui.label.text = "Hello, Anko!"
}
}
class MainActivityUi() : AnkoComponent<MainActivity> {
val textViewId = View.generateViewId()
lateinit var label : TextView // 非null型
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
label = textView {
}
}
}
}
イミュータブルにしたい
lateinit修飾子はミュータブルなプロパティ(varで宣言されたもの)にしかつけることができません。
上記のlabelをイミュータブルなプロパティ(valで宣言されたもの)にするにはどうしたらいいでしょうか。
委譲プロパティ(Delegated Properties)を利用するとvalで宣言することができます。
class MainActivityUi() : AnkoComponent<MainActivity> {
lateinit var root : View
val textViewId = View.generateViewId()
val label : TextView by lazy { // 委譲プロパティ(Delegated Properties)
root.find<TextView>(textViewId)
}
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
textView {
id = textViewId
}
}.apply {
this@MainActivityUi.root = this
}
}
}
by lazy
を指定すると、プロパティの使用時まで初期化を遅延させることができます。
プロパティが最初に呼び出された際に、lazyの直後の括弧の中身が呼び出され初期化が行われます。
lateinitはプロパティが最初に呼び出されるまでにプログラマの責務で初期化し、初期化されていない場合は例外が投げられます。
lazyはプロパティを最初に呼び出した際に初期化がされるという違いがあります。
ちなみにここでは、lazyに渡している関数の中でfindViewById()を行っています。
findという関数になっていますがこれはAnkoのライブラリに含まれる拡張関数の一つで、findViewById()をラップしています。
valで宣言できる代わりに、findViewById()を呼び出さなくてもいいというAnkoのメリットが犠牲になっています。
KotterKnife
ButterKnifeは上記のようなViewのインスタンスとプロパティの結びつけ(View Binding)を簡略化できるライブラリです。
KotterKnifeはKotlinでもButterKnifeと同等の機能が使えるライブラリであり、委譲プロパティの仕組みを使って実装されています。
AnkoをKotterKnifeと組み合わせた例は以下になります。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ui = MainActivityUi(this)
setContentView(ui.createView(
AnkoContext.Companion.create(this, this@MainActivity)
).rootView)
ui.label.text = "Hello, Anko!"
}
}
class MainActivityUi(context: Context) :
FrameLayout(context), AnkoComponent<MainActivity> {
val textViewId = View.generateViewId()
val label : TextView by bindView(textViewId) // KotterKnife
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
textView {
id = textViewId
text = "Dummy."
}
}.apply { this@MainActivityUi.addView(this) }
}
}
大きな違いはMainActivityUiがFrameLayoutのサブクラスになっていることです。
KotterKnifeのbindView関数がViewの拡張関数だったためこのような形にしました。
MainActivityUiのcreateView関数の中でUIを作成しています。
apply関数の中では、作成したUIをMainActivityUi自身にaddViewしています。
MainActivityの中で普通にui.setContentView(this)
を呼び出すと例外が投げられます。
MainActivityUiのcreateview関数で作成したViewがすでにMainActivityUi自身の子ビューであるため他の親を持てないためです。
代わりにrootViewを取得してActivityにsetContentView()しています。
なお、上記のコードだとPreviewが機能してくれませんでした。
Previewが動いてくれなかったのが悲しかったので別の方法を試しました。
せっかくなので書いておきます。
KotterKnifeのbindView関数は、ViewやActivityの拡張関数として定義されています。
指定のIDのViewが見つからなかった際の例外処理などが入っていますが、先ほどのlazyに渡す関数の中でfindViewByIdを呼ぶのとやりたいことは同じと言えそうです。
これと同等の機能をAnkoComponentの拡張関数として定義してみましょう。
ただしAnkoComponentはプロパティを持たないインターフェースであり、そのままではfindViewByIdをするべき対象を持ちません。
そこで適当な抽象クラスUiComponentを作成し、このクラスにrootとなるViewを持たせることにしました。
abstract class UiComponent<T : Context> : AnkoComponent<T> {
lateinit var root: View
}
さらにUiComponentクラスの拡張関数を用意します。
inline fun <reified V : View> UiComponent<*>.bind(id: Int): kotlin.Lazy<V> =
lazy {
root.find<V>(id)
}
今までのAnkoComponentクラスはUiComponentのサブクラスに変更しました。
class MyActivityUI() : UiComponent<MainActivity>() {
// 省略
override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
verticalLayout {
// 省略
}.apply { this@MyActivityUI.root = this }
}
}
この場合にはPreviewが動作しました。