Kotlin の DialogFragment でダイアログ表示

 

KotlinのDialogFragmentでダイアログ表示
KotlinのDialogFragmentでダイアログ表示

この記事では、KotlinのDialogFragmentを使って、Androidアプリでダイアログ表示するやり方を解説します。「リストメニュー」「チェックボックス」「ラジオボタン」「ログイン入力」などの、すべてのダイアログの作り方をご紹介します。また、アクティビティへコールバックする方法も解説しましたので、ぜひご参考になさってみてください。

はじめに

Androidアプリでダイアログを表示させるには、Dialogクラスを継承したAlertDialogを使います。アクティビティで直接インスタンスを生成するのではなく、DialogFragmentというクラスを経由してインスタンスを生成することになります。DialogFragmentクラスはフラグメントを継承しますので、アクティビティのライフサイクルと同期されます。

フラグメントに慣れてない方はKotlin で Fragment を理解しようをお読みになってからDialogFragmentを使うとスムーズかもしれません。フラグメントを使ったほうが、DialogFragmentでカプセル化されますし、アクティビティの肥大化が防げて管理しやすくなります。

シンプルなダイアログ

シンプルなダイアログ
シンプルなダイアログ

シンプルなダイアログを作るため、DialogFragmentクラスを継承したSimpleDialogFragmentクラスを作成します。onCreateDialogメソッドをオーバーライドし、その中でAlertDialogのインスタンスをビルダーで生成しています。

class SimpleDialogFragment: DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        builder.setTitle("Here Title")
            .setMessage("Here Message")
            .setPositiveButton("done") { dialog, id ->
                println("dialog:$dialog which:$id")
            }
            .setNegativeButton("cancel") { dialog, id ->
                println("dialog:$dialog which:$id")
            }

        return builder.create()
    }

}

アクティビティからは次のようにしてダイアログフラグメントを呼び出します。

val dialog = SimpleDialogFragment()
dialog.show(supportFragmentManager, "simple")

これ以降のダイアログでも、アクティビティからの呼び出しはすべてこの方法で行います。

リストメニューのダイアログ

リストメニューのダイアログ
リストメニューのダイアログ

さきほどのシンプルなダイアログでは、setPositiveButtonsetNegativeButtonでボタンの追加を行いました。今回のリストメニューのダイアログでは、setItemsメソッドを使ってリスト配列をセットしリスト表示を行っていきます。またsetMessageを実装してしまうと、リストが表示されませんのでご注意ください。

class ListDialogFragment: DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        builder.setTitle("Here Title")
            // .setMessage("Here Message") // setMessageは使うとリスト表示されないので注意!
            .setItems(R.array.language_array) { dialog, which ->
                val langs = resources.getStringArray(R.array.language_array)
                println(langs[which])

            }

        return builder.create()
    }

}

setItemsメソッドの第二引数は、クリックされたときの処理をラムダ式を渡すことになっています。このラムダ式は、次のようなOnClickListenerインタフェースで定義されてます。また、ラムダ式内の第二引数(which)には、クリックされた配列のインデックスが渡されますので、その情報を元にどのリストがクリックされたのかを判定します。

interface OnClickListener {
/**
* This method will be invoked when a button in the dialog is clicked.
*
* @param dialog the dialog that received the click
* @param which the button that was clicked (ex.
*              {@link DialogInterface#BUTTON_POSITIVE}) or the position
*              of the item clicked
*/
    void onClick(DialogInterface dialog, int which);
}

チェックボックスのダイアログ

チェックボックスのダイアログ
チェックボックスのダイアログ

チェックボックスタイプのダイアログにはsetMultiChoiceItemsを使用します。setMultiChoiceItemsの第二引数には、あらかじめチェック状態を設定するためbooleanArray型で初期値を渡すことができます。

class CheckboxDialogFragment : DialogFragment() {

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

        val checkedItems = booleanArrayOf(false, true ,false) // 保存されたデータに置き換えることができる
        val mSelectedItems:MutableList<Int> = mutableListOf()
        setupSelectedItems(checkedItems, mSelectedItems)


        val builder = AlertDialog.Builder(activity)
        builder.setTitle("Here Title")
            // .setMessage("Here Message") // setMessageは使うとリスト表示されないので注意!
            .setMultiChoiceItems(R.array.language_array, checkedItems) { dialog, which, isChecked ->
                if (isChecked) {
                    mSelectedItems.add(which)
                } else {
                    mSelectedItems.remove(which)
                }
            }
            .setPositiveButton("OK") { dialog, id ->
                printSelectedStatus(mSelectedItems)
            }
            .setNegativeButton("Cancel") { dialog, id ->

            }


        return builder.create()
    }

    private fun setupSelectedItems(
        checkedItems: BooleanArray,
        mSelectedItems: MutableList<Int>
    ) {
        var index = 0
        checkedItems.forEach {
            if (it) {
                mSelectedItems.add(index)
            }
            index++
        }
    }

    private fun printSelectedStatus(mSelectedItems: MutableList<Int>) {
        val langs = resources.getStringArray(R.array.language_array)
        mSelectedItems.forEach {
            println(langs[it])
        }
    }
}

ラジオボタンのダイアログ

ラジオボタンのダイアログ
ラジオボタンのダイアログ

ラジオボタンタイプのダイアログではsetSingleChoiceItemsを使います。setSingleChoiceItemsの第二引数では、デフォルトで選択させたいラジオボタンのインデックスをint型で渡すことができます。

class RadiobuttonDialogFragment: DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        
        val builder = AlertDialog.Builder(activity)
        builder.setTitle("Here Title")
            .setSingleChoiceItems(R.array.language_array, 1) { dialog, which: Int ->
                println(which)
            }
            .setPositiveButton("OK") { dialog, id ->
            }
            .setNegativeButton("Cancel") { dialog, id ->

            }


        return builder.create()
    }
}

【コラム】Dialogとは

ここで閑話休題です。英単語のDialogについて少しお話しましょう。

Dialogとは、Dialogueとも呼び、そのの語源はlogの「話す」から来ているそうです。logとは、ギリシャ語で「話す」や「単語」「会話」を意味します。

また、diaは「間で」の意味がありますので、「dia(間で)」+「log(話す)」となり、Dialogは「対話」や「意見交換」といった意味になります。

英英辞書でDialogを調べると「(a) written conversation in a book or play」の一文がありまして、つまりは、本やゲームでの「書かれた対話」でもあります。

【コラム】Dialogとは
【コラム】Dialogとは

また、logには「log house」にも使われるように「木」という意味もっています。昔は、文字を木に書いていたこともあるので、log自体に「対話(書かれた)」の意味をもつのは自然なことのように思われますね。

アプリでのDialog表示は、ユーザーさんとの対話になりますので、一方的なスパムアラートにならないよう十分配慮しましょうね。

ちなみに、logを語源とする単語は、Dialog以外にもたくさん存在します。catalog(カタログ)、logo(ロゴ)、logic(論理)、apology(謝罪)、prologue(序幕)、epilogue(結びの言葉)、monologue(独白)などなど。

プログラミングで出てくる英語も、語源を知っておくと意図していることが分かりやすくなりますので、お暇なときにでも、英単語の語源を学んでみるのをオススメします!

カスタムレイアウトのダイアログ(ログイン入力)

カスタムレイアウトのダイアログ(ログイン入力)
カスタムレイアウトのダイアログ(ログイン入力)

ダイアログのレイアウトは、自由にカスタマイズすることもできます。ここでは公式ドキュメントにならって、ログイン入力用のダイアログを実装しました。

まず、ダイアログ内に表示させたいレイアウトを、レイアウトファイルdialog_signin.xmlとして次のように作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content">

    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:id="@+id/email"
            android:hint="Email"/>
    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPassword"
            android:id="@+id/password"
            android:hint="Password"/>
</LinearLayout>

次に、作成したレイアウトをDialogFragmentクラスの中でinflaterを使ってinflateします。つまりdialog_signin.xmlレイアウトを元にしたViewを生成させます。丁寧にプログラムを追えば、何も難しいことはないはずです。

class SigninDialogFragment:DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val builder = AlertDialog.Builder(activity)
        val inflater = activity!!.layoutInflater
        val signinView = inflater.inflate(R.layout.dialog_signin, null)

        builder.setView(signinView)
            .setTitle("Sign in")
            .setPositiveButton("OK") { dialog, id ->
                val email = signinView.findViewById<EditText>(R.id.email).text
                val password = signinView.findViewById<EditText>(R.id.password).text
                println("Email: $email Password:$password")
            }
            .setNegativeButton("Cancel") { dialog, id ->

            }

        return builder.create()
    }
}

ダイアログからアクティビティへコールバック

ダイアログからアクティビティへコールバック
ダイアログからアクティビティへコールバック

最後に、アクティビティへコールバック可能なダイアログを作成してみましょう。

インタフェースを使ってコールバックを実装することで、アクティビティとダイアログを疎結合になりますので管理がしやすくなります。ここでは最初の「シンプルなダイアログ」のプログラムをベースに改良してあります。

class NoticeDialogFragment: DialogFragment() {

    public interface NoticeDialogLister {
        public fun onDialogPositiveClick(dialog:DialogFragment)
        public fun onDialogNegativeClick(dialog:DialogFragment)
    }

    var mLister:NoticeDialogLister? = null

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        try {
            mLister = context as NoticeDialogLister
        } catch (e: ClassCastException) {
            throw ClassCastException("${context.toString()} must implement NoticeDialogListener")
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        builder.setTitle("Here Title")
            .setMessage("Here Message")
            .setPositiveButton("ok") { dialog, id ->
                println("dialog:$dialog which:$id")
                mLister?.onDialogPositiveClick(this)
            }
            .setNegativeButton("cancel") { dialog, id ->
                println("dialog:$dialog which:$id")
                mLister?.onDialogNegativeClick(this)
            }

        return builder.create()
    }


    override fun onDestroy() {
        println("NoticeDialogFragmentのonDestroyが呼ばれたよ!")
        super.onDestroy()
    }

    override fun onDetach() {
        println("NoticeDialogFragmentのonDetachが呼ばれたよ!")
        super.onDetach()
	mLister = null

    }
}

上記のプログラムをすこし詳しく見ていきましょう。

まず、コールバック用のインターフェイスNoticeDialogListerを作り、DialogFragmentを継承したNoticeDialogFragmentクラス内に作成します。

class NoticeDialogFragment: DialogFragment() {

    public interface NoticeDialogLister {
        public fun onDialogPositiveClick(dialog:DialogFragment)
        public fun onDialogNegativeClick(dialog:DialogFragment)
    }
    ...

このインタフェースはダイアログを呼び出すホスト、つまりMainActivityへ実装しておきます。

class MainActivity : AppCompatActivity(), NoticeDialogFragment.NoticeDialogLister {

    override fun onDialogPositiveClick(dialog: DialogFragment) {
        println("NoticeDialogでOKボタンが押されたよ!")
    }

    override fun onDialogNegativeClick(dialog: DialogFragment) {
        println("NoticeDialogでCancelボタンが押されたよ!")
    }
	...

onAttachでは、MainActivity(ここではContextになっています)をNoticeDialogListerへキャストしています。ホストのActivityNoticeDialogListerインタフェースが実装されていなければ、例外が起きてスローされるようになってます。このことで、NoticeDialogFragment.NoticeDialogListerのアクティビティへの実装忘れを防げます。

	...
    override fun onAttach(context: Context?) {
        super.onAttach(context)
        try {
            mLister = context as NoticeDialogLister
        } catch (e: ClassCastException) {
            throw ClassCastException("${context.toString()} must implement NoticeDialogListener")
        }
    }
	...

そしてダイアログのボタンクリックイベント内で、NoticeDialogListerのメソッドを実行すれば、フラグメントからアクティビティへの通知が可能になります。

...
.setPositiveButton("ok") { dialog, id ->
                println("dialog:$dialog which:$id")
                mLister?.onDialogPositiveClick(this)
            }
...
このようなコールバックの実装パターンは、iOS開発では頻繁に行われるデリゲートでのコールバックとよく似ています。Objective-C/Swiftのprotocolが、Java/Kotlinのinterfaceに対応しているイメージです。
また、Swiftでは強参照を避けるためにweakを指定しますが、AndroidでのFragmentを通した実装ではその必要はないようです。ダイアログが使い終われば、DialogFragment内のonDestroyonDetachが呼ばれますので、開放忘れの心配はなさそうです。

以上でKotlinのDialogFragmentを使って、Androidアプリでダイアログ表示するやり方の説明をおわります。本プロジェクトは、 GitHubリポジトリ で公開してますので、ご参考になさってみてください。。

▼ こんな記事も書いてます。

記事に関するご質問などがあれば、
@tosisico または お問い合わせ までご連絡ください。
関連記事