安全にインスタンスのライフサイクルを管理する

f:id:numanuma08:20220222150631j:plain

こんにちは、id:numanuma08です。気がつけば2月も後半ですね。場所によっては春を感じる頃でしょうか。山梨はまだまだ寒いです。そんなわけで、今日もAndroidネタです。

ライフサイクルの中でインスタンスを管理するには、注意が必要

Androidのライフサイクル関連の処理はViewModelやLifecycleコンポーネント、Navigationコンポーネントの登場によってだいぶ簡単になりました。しかし、完全に考慮しなくていいわけではありません。例えば、Fragmentが特定の条件下だけ表示するPopupWindowをもつ場合を考えてみます。

class MyFragment(R.layout.my_fragment) {

  var popupWindow: PopupWindow? = null // (1)

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    popupWindow = PopupWindow(requireContext())

   viewModel.someEvent.observe(viewLifecycleOwner) {
     popupWindow?.show() // (2)
    }
  }

  override fun onDestroyView() {
    popupWindow?.dismiss()
    popupWindow = null // (3)
  }
}

簡単なコードですがこのようになります。まずpopUpWindowはonViewCreatedで初期化するため、nullableで宣言が必要です(1)。nullableで宣言しているため、この場所では非nullであっても?.を付ける必要があります(2)。最後に、インスタンス化されたpopupWindowはnullで明示的にインスタンス破棄しないとメモリリークに繋がります(3)。また、nullを最後に代入するため、lateinitで宣言はできません。

このように、コードの書きにくさとちょっとの不注意でメモリリークを起こす危険性があります。どうするべきでしょうか。

安全にライフサイクルの中でインスタンスを管理する方法の提案

解決策ですが、databinding-ktxというライブラリで行われている方法がそのまま参考になりました。View.setTagしたインスタンスはViewが破棄されたときに一緒に破棄されます。この仕組を使って以下の拡張関数を作りました。

/**
 * FragmentでViewのライフサイクルが生きている間だけ保持したいプロパティを生成、再利用する。
 * 初回アクセス時にインスタンスを生成して、Viewが削除されるときにインスタンスを破棄する
 *
 * @param tagId viewに紐つけるときに使うid。Viewの中でユニークな物を使うため、idリソースを利用する。未指定の場合、[R.id.fragment_lifecycle_property]が利用されるが、複数の[lifecycleProperty]をFragmentで持つときは任意のidを渡す
 * @param generator ライフサイクルで管理したいインスタンスを生成する関数
 * */
fun <T> Fragment.lifecycleProperty(
    @IdRes tagId: Int = R.id.fragment_lifecycle_property,
    generator: () -> T
) =
    ReadOnlyProperty<Fragment, T> { _, _ ->
        @Suppress("UNCHECKED_CAST")
        (requireView().getTag(tagId) as? T)?.let { return@ReadOnlyProperty it }
        requireView().let {
            val value = generator()
            // tagにセットされたインスタンスはViewが破棄されたときに一緒に破棄される
            // https://github.com/wada811/DataBinding-ktx/issues/7
            it.setTag(tagId, value)
            value
        }
    }

これを使うと、先程のサンプルは以下のようになります。

class MyFragment(R.layout.my_fragment) {

  val popupWindow: PopupWindow by lifecycleProperty {  PopupWindow(requireContext()) }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   viewModel.someEvent.observe(viewLifecycleOwner) {
     popupWindow.show()
    }
  }

  override fun onDestroyView() {
    popupWindow.dismiss()
  }
}

nullableなプロパティがなくなり、そしてリークのリスクがなくなりました。

まとめ

ライフサイクルの中で安全にインスタンスを管理する方法を提案しました。