LoginSignup
10
6

More than 5 years have passed since last update.

FragmentではKotterKnifeをうかつに使えないという話

Last updated at Posted at 2016-04-12

KotlinでViewのバインドをするのに、ButterKnifeの代わりにKotterKnifeを使うというのは、素直な選択だと思うのですが、Fragmentで使う場合、Fragmentの性質とKotterKnifeの実装の関係から、意図したとおりには動作しないことがあります。

KotterKnifeはなにをしてくれる?

KotterKnifeでは、KotlinのDelegationを利用して、変数の初期化を遅延して、必要なときにfindViewById()してくれます。
ただし、一度、findViewById()で見つけたViewはキャッシュ(メモ化)して、それ以後はキャッシュしたViewを返すようになっています。

Fragmentのライフサイクルを思い出してみよう

Fragmentで画面遷移をする場合、遷移先から戻ってくるとき(バックスタックから戻されるとき)、onCreateView()が呼ばれます。
通常、そこではリソースで定義したレイアウトをinflate()して返します。

なにが問題?

KotterKnifeがfindViewById()するのは、最初の一回だけです。
Fragmentでは、Viewを生成するのは一回だけとは限りません。

つまり、Fragmentが抱えるViewとFragment内で変数にバインドされたViewが異なってしまう場合があるわけです。というか、onCreateView()でViewを作り直している限り、遷移先から戻ってきたFragmentでは、必ず異なるものになってしまいます。

結局

KotterKnifeを使うのはやめました。

キャッシュをクリアできるdelegateならいけそうなので、

ViewDelegate.kt

class ViewDelegate(var rootView: View? = null) {

    private val delegates = ArrayList<Bind<*>>()

    fun <V: View> bind(id: Int): Bind<V> {
        val delegate = Bind<V>(id)
        delegates.add(delegate)
        return delegate
    }

    fun clearAll() {
        delegates.forEach { it.clear() }
    }

    inner class Bind<V: View>(val id: Int) : ReadOnlyProperty<Any, V> {
        var root: View? = null
        var view: View? = null

        @Suppress("UNCHECKED_CAST")
        override fun getValue(thisRef: Any, property: KProperty<*>): V {
            if (view == null || root != rootView) {
                root = rootView
                if (root != null) {
                    view = root!!.findViewById(id)
                } else {
                    throw IllegalStateException("root view is null.")
                }
            }
            if (view != null) {
                return view!! as V
            } else {
                throw IllegalStateException("View ID $id for '${property.name}' not found.")
            }
        }

        fun clear() {
            root = null
            view = null
        }
    }
}

こんなクラスを作って、

FooFragment.kt

class FooFragment : Fragment() {
    val views = ViewDelegate()
    val hogeButton: Button by views.bind(R.id.hoge_button)
    val fugaText: TextView by views.bind(R.id.fuga_text)

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view inflater.inflate(R.layout.fragment_foo, container, false)
        views.rootView = view
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        views.clearAll()
    }

    ...
}

こんな風に使うようにしました。

BindクラスのインスタンスがViewを保持するので、メモリリークしないようにonDestroyView()でclearAll()してますが、定型処理なのでちょっといやんな感じですね。
ViewHolderパターンでも使えるように、rootViewを保持しているのですが、Fragmentを保持するようにすれば、views.rootView = viewとしているところも省けそうです。
誰か、いい感じにまとめてくれる人が現れるとうれしいですね。

2016.5.23 追記

Delegateについて、いろいろ紆余曲折あって、今では、

Views.kt
@Suppress("UNCHECKED_CAST")
object Views {

    fun <V: View> bind(activity: Activity, id: Int): Lazy<V> = lazy { activity.findViewById(id) as V }
    fun <V: View> bind(fragment: android.support.v4.app.Fragment, id: Int): Bind4<V> = Bind4(fragment, id)
    fun <V: View> bind(fragment: android.app.Fragment, id: Int): Bind<V> = Bind(fragment, id)
    fun <V: View> bind(view: View, id: Int): Lazy<V> = lazy { view.findViewById(id) as V }

    class Bind<V: View>(val fragment: android.app.Fragment, val id: Int) : ReadOnlyProperty<Any, V> {

        override fun getValue(thisRef: Any, property: KProperty<*>): V {
            return fragment.view?.findViewById(id) as V
        }
    }

    class Bind4<V: View>(val fragment: android.support.v4.app.Fragment, val id: Int) : ReadOnlyProperty<Any, V> {

        override fun getValue(thisRef: Any, property: KProperty<*>): V {
            return fragment.view?.findViewById(id) as V
        }
    }
}

こんなobjectを作って、

FooFragment.kt

class FooFragment : Fragment() {
    val hogeButton: Button by Views.bind(this, R.id.hoge_button)
    val fugaText: TextView by Views.bind(this, R.id.fuga_text)

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_foo, container, false)
    }

    ...
}

こんな風に使っています。

Fragmentの場合には、毎回、findViewById()していて効率は悪いのですが、clearAll()を忘れずに記述するというプレッシャーが心地よくないので、効率についてはまぁいいやと思うことにしています。

10
6
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
10
6