LoginSignup
1
1

More than 5 years have passed since last update.

AnkoとKotterKnifeを組み合わせる

Posted at

AnkoComponent実装クラスのプロパティ宣言をなんとかしたい

AnkoComponent実装クラスをViewHolderのように使用する時には、外から使用したいViewをプロパティとして宣言しています。これをKotlinらしいコードにするために一工夫してみます。

NotNullにしたい

以下のようなコードがあったとします。

MainActivity.kt
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修飾子を使うと以下のようにできます。

MainActivity.kt
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で宣言することができます。

MainActivity.kt
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と同等の機能が使えるライブラリであり、委譲プロパティの仕組みを使って実装されています。

JakeWharton/kotterknife

AnkoをKotterKnifeと組み合わせた例は以下になります。

MainActivity.kt
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が動作しました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1