Android(Kotlin) Fragment にコールバックをセットする無難な方法

2020/11/13

はじめに

画面回転でタップが効かなくなるバグに遭遇し、Fragment にコールバックをセットする方法を改めて考えさせられました。最初に結論を言ってしまうと、多少面倒でも良く見かける下記方式を実直に実装するのが無難で良いかと考えています。

  1. 参照が循環しないようにリスナーインターフェースを用意。
  2. Fragment ライフサイクルメソッド onAttach() 内で、context(Activity)もしくは親 Fragment をチェック。
  3. リスナーインターフェースを実装しているなら、リスナー登録。
  4. 該当イベント発生時に登録済みリスナーを通して処理を呼び出し。

実例

バグ内容

ViewPager2 を通して生成する Fragment におけるタップ時処理を外から渡していたのですが、画面回転を挟むと該当処理が呼ばれなくなってしまいました。

バグ原因

画面回転後にシステムが該当 Fragment を勝手に再インスタンス化してしまうため、明示的なインスタンス化後に行なっていたタップ時処理渡しがスキップされてしまい、タップ時に何の処理も呼ばれなくなってしまったということです。

修正方針

とにかく、明示的にインスタンス化しようが、システムが勝手にインスタンス化しようが、必ず通る処理(ライフサイクルメソッド等)でタップ時処理を紐付けることが必須となります。

いくつか選択肢が考えられますが、前述した内容(ただし、親 Fragment でリスナー実装)で対応しました。

修正内容

ViewPager2 を利用する側の Fragment

-class TaskListFragment : Fragment(R.layout.fragment_task_list) {
+class TaskListFragment : Fragment(R.layout.fragment_task_list), PlaceholderFragment.OnItemClickListener {

     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        val sectionsPagerAdapter = SectionsPagerAdapter(this, ::onItemClick)
+        val sectionsPagerAdapter = SectionsPagerAdapter(this)
         // 以下、省略
     }

-
-    private fun onItemClick(taskId: Int) {
+    override fun onItemClick(taskId: Int) {
         // 省略
     }

Adapter

-class SectionsPagerAdapter(fragment: Fragment, private val onItemClick: (Int) -> Unit)
-    : FragmentStateAdapter(fragment) {
+class SectionsPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

     override fun getItemCount(): Int = 3

     override fun createFragment(position: Int): Fragment {
-        return PlaceholderFragment.newInstance(position).apply {
-            setOnItemClickListener(onItemClick)
-        }
+        return PlaceholderFragment.newInstance(position)
     }
 }

Fragment

 class PlaceholderFragment : Fragment() {
+    interface OnItemClickListener {
+        fun onItemClick(taskId: Int)
+    }

+    private lateinit var onItemClickListener: OnItemClickListener
-    private var onItemClick: ((Int) -> Unit)? = null

+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        when (parentFragment) {
+            is OnItemClickListener -> onItemClickListener = parentFragment as OnItemClickListener
+        }
+    }
+
-    internal fun setOnItemClickListener(onItemClick: (Int) -> Unit) {
-        this.onItemClick = onItemClick
-    }
-
     private fun onItemClick(taskId: Int) {
-        onItemClick?.invoke(taskId)
+        onItemClickListener.onItemClick(taskId)
     }

おわりに

システムが勝手に Fragment をどう扱うかを把握していなかったがために引っかかったバグをきっかけに、Fragment にコールバックをセットする方法を改めて考えてみました。勝手に再インスタンス化ってちょっと独特ですよね。

おまけ

Kotlin における可視性修飾子みたいな概念がある場合、極力アクセスを制限したくなるのがエンジニアの性ですが、Fragment のコンストラクタを private にしてしまうと、今回のお話の通り、画面回転後に勝手にインスタンス化を試みられた結果、落ちてしまいます。private にしたくなる気持ちはグッと抑えましょう・・・。