4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DialogFragmentの利用方法

Last updated at Posted at 2020-02-17

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

前提

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

スクリーンショット 2020-02-17 0.43.17.png

修正前

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から取得するようにしました。

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にリネームしています。

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内では意識しないで使えるように拡張関数を用意して利用するようにしました。

// 拡張関数 フラグメントが設定されていた場合はそちらを利用する
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の役割など修正の余地はまだあるかと思いますが、ひとまず最低限の拡張性と安定性は確保できたと思います。

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も考慮したほうが良さそう


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

まとめ

  • publicでsetしている変数を見つけたら警戒する
  • 拡張関数を用意したものの、そのあたりの抽象化はもっといいやり方がありそう
4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?