DialogFragmentの利用方法

プロダクトの既存コードで使われている`DialogFragment`を改善したので、その修正内容をメモしておきます

# 前提
実装予定のダイアログは以下のようなごく一般的なものとする

<img width="400" alt="スクリーンショット 2020-02-17 0.43.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168872/651c52a5-eb1b-e713-1882-aab0d77f3917.png">

# 修正前

```kotlin:UpdateConfirmDialogFragment.kt
class UpdateConfirmDialogFragment : DialogFragment() {
    var mTitle: String? = null 
    private var listener: OnFragmentInteractionListener? = null

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(activity!!)
            .setTitle(mTitle)
            .setPositiveButton("ok") { _, _ ->
                listener?.onClickConfirmOk(confirmType)
            }
            .setNegativeButton("cancel") { _, _ ->
                listener?.onClickConfirmCancel(confirmType)
            }
            .create()
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }

    fun setListener(listener: OnFragmentInteractionListener){
        this.listener = listener
    }

    interface OnFragmentInteractionListener {
        fun onClickConfirmOk()
        fun onClickConfirmCancel()
    }
}
```

※プロダクトコードを思い出しながら修正後のコードから作ったコードなので、誤りがあるかもしれません

以下、修正内容です

# 1. interfaceの設定方法変更
androidのactivity/fragmentは、インスタンス破棄後の復帰を考慮してライフサイクルメソッドで値を設定するべきです。今回のコードでは
ダイアログタイトルがpublic変数、イベントコールバック用のinterfaceがpublicメソッドを使って設定されていましたが、それぞれargument及びcontextから取得するようにしました。

```kotlin:UpdateConfirmDialogFragment.kt
    private val mTitle by lazy {
        arguments?.getString(ARG_Title, "") ?: ""
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = context as? OnFragmentInteractionListener
    }

    companion object {
        private const val ARG_TITLE = "ARG_TITLE"
        fun newInstance(title: String) =
            ConfirmDialogFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_TITLE, title)
                }
            }
    }
```

# 2. 類似要件のダイアログも生成できるようにする
修正を進めていたところ、ダイアログ自体の要件がほぼ変わらないDelete用のダイアログがあることがわかりました。そのため、インスタンス作成時にタイトルではなくTypeを渡すようにし、タイトルはその変数として保持させるようにしました。また、それをコールバックの引数として設定し、利用側で判別できるようにしました(※同一Activity内で2種類のダイアログ表示があった際の考慮です)。Fragment名は、より汎用的にするために`ConfirmDialogFragment`にリネームしています。

```kotlin:ConfirmDialogFragment.kt
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(activity!!)
            .setTitle(confirmType?.title)
            .setPositiveButton("ok") { _, _ ->
                listener?.onClickConfirmOk(confirmType)
            }
            .setNegativeButton("cancel") { _, _ ->
                listener?.onClickConfirmCancel(confirmType)
            }
            .create()
    }

    interface OnFragmentInteractionListener {
        fun onClickConfirmOk(confirmType: ConfirmType?)
        fun onClickConfirmCancel(confirmType: ConfirmType?)
    }

    sealed class ConfirmType : Serializable {
        abstract val title: String

        object Update : ConfirmType() {
            override val title = "更新しますか?"
        }

        object Delete : ConfirmType() {
            override val title = "削除しますか?"
        }
    }

    companion object {
        private const val ARG_CONFIRM_TYPE = "ARG_CONFIRM_TYPE"
        fun newInstance(targetFragment: Fragment? = null, confirmType: ConfirmType) =
            ConfirmDialogFragment().apply {
                arguments = Bundle().apply {
                    putSerializable(ARG_CONFIRM_TYPE, confirmType)
                }
            }
    }
```
# 3. コールバック先を選択できるようにする
更に実装を進めていると、このダイアログはActivityとFragment、どちらからも呼ばれる可能性があることがわかりました。activityはこれまで通り`context`から、fragmentは`argument`に設定することでtargetとして取得できるようになります。fragmentの設定をオプションとして用意し、DialogFragment内では意識しないで使えるように拡張関数を用意して利用するようにしました。

```kotlin
// 拡張関数 フラグメントが設定されていた場合はそちらを利用する
fun <T> DialogFragment.getTarget() = (targetFragment ?: context) as? T

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = getTarget()
    }

    companion object {
        fun newInstance(targetFragment: Fragment? = null, confirmType: ConfirmType) =
            ConfirmDialogFragment().apply {
                arguments = Bundle().apply {
                    putSerializable(ARG_CONFIRM_TYPE, confirmType)
                }
                setTargetFragment(targetFragment, 0)
            }
    }


```

# 修正後

ごちゃごちゃ言いながらリファクタしてきましたが、最終的には以下のようになりました。変数名やConfirmTypeの役割など修正の余地はまだあるかと思いますが、ひとまず最低限の拡張性と安定性は確保できたと思います。

```kotlin:DialogFragment.kt
class ConfirmDialogFragment : DialogFragment() {
    private val confirmType by lazy {
        arguments?.getSerializable(ARG_CONFIRM_TYPE) as? ConfirmType
    }
    private var listener: OnFragmentInteractionListener? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = getTarget()
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(activity!!)
            .setTitle(confirmType?.title)
            .setPositiveButton("ok") { _, _ ->
                listener?.onClickConfirmOk(confirmType)
            }
            .setNegativeButton("cancel") { _, _ ->
                listener?.onClickConfirmCancel(confirmType)
            }
            .create()
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }

    interface OnFragmentInteractionListener {
        fun onClickConfirmOk(confirmType: ConfirmType?)
        fun onClickConfirmCancel(confirmType: ConfirmType?)
    }

    sealed class ConfirmType : Serializable {
        abstract val title: String

        object Update : ConfirmType() {
            override val title = "更新しますか?"
        }

        object Delete : ConfirmType() {
            override val title = "削除しますか?"
        }
    }

    companion object {
        private const val ARG_CONFIRM_TYPE = "ARG_CONFIRM_TYPE"
        fun newInstance(targetFragment: Fragment? = null, confirmType: ConfirmType) =
            ConfirmDialogFragment().apply {
                arguments = Bundle().apply {
                    putSerializable(ARG_CONFIRM_TYPE, confirmType)
                }
                setTargetFragment(targetFragment, 0)
            }
    }
}

fun <T> DialogFragment.getTarget() = (targetFragment ?: context) as? T

```

**追記**
拡張関数部分は、targetFragmentを取得できない場合のためにparentFragmentも考慮したほうが良さそう

```kotlin

fun <T> DialogFragment.getTarget() = (targetFragment ?: parentFragment ?: context) as? T
```



# まとめ
- publicでsetしている変数を見つけたら警戒する
- 拡張関数を用意したものの、そのあたりの抽象化はもっといいやり方がありそう