シンプルで汎用的なDialogFragmentを作りたい

Androidのダイアログの実装って面倒ですよね。簡単なメッセージとOKボタンを表示したいだけなのに、DialogFragmentをのサブクラスを実装しないといけません。アプリのあちこちでダイアログを使っていると、似たような実装があちこちにできてしまいがちです。

そこで今回は、シンプルな(タイトル・メッセージ・ボタン2つ)ダイアログを、簡単に(いちいちサブクラスを作ることなく)実装できる、汎用的なDialogFragmentを作りました。

概要

クラス名:SimpleDialog

SimpleDialogクラスは、ダイアログのタイトル、メッセージ、Positive/Negativeボタンのテキストを指定できるダイアログです。

SimpleDialog.Builderクラスを使ってこれらのパラメータを設定し、SimpleDialog.Builder#create()関数でSimpleDialogのインスタンスを作成します。あとは通常のDialogFragmentと同じようにshow()関数で表示します。

呼び出し側のActivityまたはFragmentSimpleDialog.ResultListenerを実装していれば、Positive/Negativeボタンを押したときにSimpleDialog.ResultListener#onSimpleDialogResult()が呼び出されるので、結果を取得することができます。

使い方

では具体的な使い方を説明します。

Activityでは、SimpleDialog.Builder()Builderクラスを取得し、setXXX()関数でパラメータを設定していきます。setXXX()関数は、引数に文字列を直接指定するか、リソースIDを指定することができます。また、必要なパラメータだけ設定すればOKです。例えばsetNegativeText()を省略した場合、OKボタン一つだけのダイアログになります。

一通りパラメータを設定したら、create()SimpleDialogのインスタンスを取得し、show()で表示します。show()DialogFragmentの関数をそのまま継承しています。

ボタンクリックの結果を受け取るには、ActivitySimpleDialog.ResultListenerを実装します。Positive/Negativeボタンがクリックされると、onSimpleDialogResult()が呼び出されます。第一引数のtagには、show()の第二引数で指定したtagが渡されます。一つのActivityで複数のダイアログを使い分ける場合は、tagで区別します。第二引数のresultは、Positive/Negativeどちらのボタンがクリックされたかを示します。

class MyActivity : AppCompatActivity(), SimpleDialog.ResultListener {

    private fun showDialog() {
        SimpleDialog.Builder()
            .setTitle("Title") // setTitle(R.string.dialog_title)のようにリソースIDの指定も可。
            .setMessage("Message")
            .setPositiveText("OK")
            .setNegativeText("CANCEL")
            .create()
            .show(supportFragmentManager, "SimpleDialog") // FragmentではchildFragmentManagerを指定する。
    }

    override fun onSimpleDialogResult(tag: String?, result: SimpleDialog.Result) {
        if (tag == "SimpleDialog") {
            when (result) {
                SimpleDialog.Result.POSITIVE -> { /* OK button is clicked */ }
                SimpleDialog.Result.NEGATIVE -> { /* CANCEL button is clicked */ }
            }
        }
    }

このように、呼び出し側では一文完結でダイアログを表示することができます。また、特にリスナーの設定などしなくても、インターフェースの実装だけすればボタンクリック時の結果を受け取ることができます。

また上記の例はActivityで表示していますが、Fragmentからも同様に表示することができます。その場合は、show()の第一引数をsupportFragmentManagerではなくchildFragmentManagerにしてください。

実装

次にSimpleDialogクラスの実装を説明します。全体のソースコードはこのページの最後に載せています。

まずはBuilderクラスの実装です。Builderクラスの役割は、SimpleDialogargumentsに渡すbundleオブジェクトを作成することです。setXXX()関数それぞれでbundleオブジェクトにパラメータを設定しています。そしてcreate()SimpleDialogオブジェクトを作成し、argumentsbundleを設定しています。こうすることによってargumentsに各パラメータが保存されるので、画面が回転したりしてダイアログが破棄された場合でも、パラメータを維持することができます。

各set関数は、引数にStringを取るものとIntを取るものをそれぞれ用意しています。これは、文字列を直接指定することも、リソースIDで指定することもできるようにするためです。

class Builder {
    private val bundle = Bundle()

    fun setTitle(title: String) = this.apply {
        bundle.putString(KEY_TITLE, title)
    }

    fun setTitle(titleId: Int) = this.apply {
        bundle.putInt(KEY_TITLE_ID, titleId)
    }

    fun setMessage(message: String) = this.apply {
        bundle.putString(KEY_MESSAGE, message)
    }

    fun setMessage(messageId: Int) = this.apply {
        bundle.putInt(KEY_MESSAGE_ID, messageId)
    }

    fun setPositiveText(text: String) = this.apply {
        bundle.putString(KEY_POSITIVE_TEXT, text)
    }

    fun setPositiveText(textId: Int) = this.apply {
        bundle.putInt(KEY_POSITIVE_TEXT_ID, textId)
    }

    fun setNegativeText(text: String) = this.apply {
        bundle.putString(KEY_NEGATIVE_TEXT, text)
    }

    fun setNegativeText(textId: Int) = this.apply {
        bundle.putInt(KEY_NEGATIVE_TEXT_ID, textId)
    }

    fun create() = SimpleDialog().apply {
        arguments = bundle
    }
}

インターフェースResultListenerには、onSimpleDialogResult()関数が一つだけ定義してあります。

interface ResultListener {
    fun onSimpleDialogResult(tag: String?, result: Result)
}

次はSimpleDialog本体です。DialogFragmentを継承し、onAttach()onCreateDialog()をoverrideしています。

onAttach()では、ボタンクリック時のイベント送信先を判定しています。ActivityからDialogFragment#show()を呼び出した場合、DialogFragmentから見るとcontextActivityになります。FragmentからDialogFragment#show()childFragmentManagerを指定して呼び出した場合、DialogFragmentから見るとparentFragmentが呼び出し側のFragmentになります。onAttach()では、これらがResultListenerを実装しているかどうかを見ています。実装していればlistenerとして登録しています。このようにonAttach()を実装しているので、呼び出し側でリスナーの登録が不要になります。また、ActivityでもFragmentでも同じように使うことができるようになります。

class SimpleDialog : DialogFragment() {

    private var listener: ResultListener? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = when {
            context is ResultListener -> context
            parentFragment is ResultListener -> parentFragment as ResultListener
            else -> null
        }
    }
}

残るはonCreateDialog()です。

若干長いですが、やっていることはシンプルで、SimpleDialog.Builderが設定したargumentsから各パラメータを取り出して設定しているだけです。ただ、ここのbuilderAlertDialog.Builderなので、混乱しないようにしてください。

リソースIDが指定されていればリソースIDを、指定されていなければ文字列を設定しています。どちらも指定されていない場合、文字列を指定する方のset関数の引数がnullになり、そのパラメータは表示されないことになります。Positive/Negativeボタンを押したときは、onAttach()で登録したlisetnerに対してonSimpleDialogResult()を呼び出しています。

class SimpleDialog : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val builder = activity?.let { AlertDialog.Builder(it) }
            ?: throw IllegalStateException("Activity is null when onCreateDialog() is called.")

        /* Title */
        val titleId = arguments?.getInt(KEY_TITLE_ID) ?: 0
        if (titleId != 0) {
            builder.setTitle(titleId)
        } else {
            builder.setTitle(arguments?.getString(KEY_TITLE))
        }

        /* Message */
        val messageId = arguments?.getInt(KEY_MESSAGE_ID) ?: 0
        if (messageId != 0) {
            builder.setMessage(messageId)
        } else {
            builder.setMessage(arguments?.getString(KEY_MESSAGE))
        }

        /* Positive Button */
        val positiveTextId = arguments?.getInt(KEY_POSITIVE_TEXT_ID) ?: 0
        if (positiveTextId != 0) {
            builder.setPositiveButton(positiveTextId) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.POSITIVE)
            }
        } else {
            builder.setPositiveButton(arguments?.getString(KEY_POSITIVE_TEXT)) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.POSITIVE)
            }
        }

        /* Negative Button */
        val negativeTextId = arguments?.getInt(KEY_NEGATIVE_TEXT_ID) ?: 0
        if (negativeTextId != 0) {
            builder.setNegativeButton(negativeTextId) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.NEGATIVE)
            }
        } else {
            builder.setNegativeButton(arguments?.getString(KEY_NEGATIVE_TEXT)) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.NEGATIVE)
            }
        }

        return builder.create()
    }
}

ソースコード全体

class SimpleDialog : DialogFragment() {

    interface ResultListener {
        fun onSimpleDialogResult(tag: String?, result: Result)
    }

    enum class Result {
        POSITIVE,
        NEGATIVE
    }

    class Builder {
        private val bundle = Bundle()

        fun setTitle(title: String) = this.apply {
            bundle.putString(KEY_TITLE, title)
        }

        fun setTitle(titleId: Int) = this.apply {
            bundle.putInt(KEY_TITLE_ID, titleId)
        }

        fun setMessage(message: String) = this.apply {
            bundle.putString(KEY_MESSAGE, message)
        }

        fun setMessage(messageId: Int) = this.apply {
            bundle.putInt(KEY_MESSAGE_ID, messageId)
        }

        fun setPositiveText(text: String) = this.apply {
            bundle.putString(KEY_POSITIVE_TEXT, text)
        }

        fun setPositiveText(textId: Int) = this.apply {
            bundle.putInt(KEY_POSITIVE_TEXT_ID, textId)
        }

        fun setNegativeText(text: String) = this.apply {
            bundle.putString(KEY_NEGATIVE_TEXT, text)
        }

        fun setNegativeText(textId: Int) = this.apply {
            bundle.putInt(KEY_NEGATIVE_TEXT_ID, textId)
        }

        fun create() = SimpleDialog().apply {
            arguments = bundle
        }
    }

    companion object {
        private const val KEY_TITLE = "KeyTitle"
        private const val KEY_TITLE_ID = "KeyTitleId"
        private const val KEY_MESSAGE = "KeyMessage"
        private const val KEY_MESSAGE_ID = "KeyMessageId"
        private const val KEY_POSITIVE_TEXT = "KeyPositiveText"
        private const val KEY_POSITIVE_TEXT_ID = "KeyPositiveTextId"
        private const val KEY_NEGATIVE_TEXT = "KeyNegativeText"
        private const val KEY_NEGATIVE_TEXT_ID = "KeyNegativeTextId"
    }

    private var listener: ResultListener? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = when {
            context is ResultListener -> context
            parentFragment is ResultListener -> parentFragment as ResultListener
            else -> null
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val builder = activity?.let { AlertDialog.Builder(it) }
            ?: throw IllegalStateException("Activity is null when onCreateDialog() is called.")

        /* Title */
        val titleId = arguments?.getInt(KEY_TITLE_ID) ?: 0
        if (titleId != 0) {
            builder.setTitle(titleId)
        } else {
            builder.setTitle(arguments?.getString(KEY_TITLE))
        }

        /* Message */
        val messageId = arguments?.getInt(KEY_MESSAGE_ID) ?: 0
        if (messageId != 0) {
            builder.setMessage(messageId)
        } else {
            builder.setMessage(arguments?.getString(KEY_MESSAGE))
        }

        /* Positive Button */
        val positiveTextId = arguments?.getInt(KEY_POSITIVE_TEXT_ID) ?: 0
        if (positiveTextId != 0) {
            builder.setPositiveButton(positiveTextId) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.POSITIVE)
            }
        } else {
            builder.setPositiveButton(arguments?.getString(KEY_POSITIVE_TEXT)) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.POSITIVE)
            }
        }

        /* Negative Button */
        val negativeTextId = arguments?.getInt(KEY_NEGATIVE_TEXT_ID) ?: 0
        if (negativeTextId != 0) {
            builder.setNegativeButton(negativeTextId) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.NEGATIVE)
            }
        } else {
            builder.setNegativeButton(arguments?.getString(KEY_NEGATIVE_TEXT)) { _,_ ->
                listener?.onSimpleDialogResult(tag, Result.NEGATIVE)
            }
        }

        return builder.create()
    }
}