【Kotlin/Android】DialogFragmentの使い方!独自レイアウトのカスタムダイアログ実装

この記事からわかること
- Android Studio/KotlinのDialogFragmentの使い方
- アプリでカスタムダイアログを実装する方法
- 独自のレイアウトで作成するには?
- データや処理を渡す方法
- showとshowNowの違いと使い方
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
以下のようなOS標準のアラートではなく、独自のレイアウトを使用したカスタムダイアログを実装する方法をまとめていきます。

カスタムダイアログ:DialogFragment
独自のレイアウトを使用したカスタムダイアログを実装するためにはDialogFragment
を使用してFragment側にダイアログの処理を記述します。まずはDialogFragment
の利用方法を理解するためにOS標準のダイアログ処理を実装してみます。
実装の手順
- DialogFragmentを継承したFragmentクラスの作成
- onCreateDialogメソッドをオーバーライドしてダイアログ構築処理を実装
- builder.create()してDialogインスタンスを取得しreturn
import android.app.Dialog
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
class CustomDialogFragment: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(this.requireContext())
builder.setTitle("ここにタイトル")
.setMessage("ダイアログのメッセージ")
.setPositiveButton("OK", { dialog, id ->
// ボタンクリック時の処理
})
.show()
return builder.create()
}
}
ダイアログ表示処理をFragmentに切り出したことで流用しやすくなりまた、「MainActivity.kt」側などから以下のようにすっきりと呼び出すことができるようになります。
val button:Button = findViewById(R.id.done_button)
button.setOnClickListener{
val dialog = CustomDialogFragment()
dialog.show(supportFragmentManager, "custom")
}

独自のレイアウトダイアログ(xml)を表示する
ダイアログとして表示するビューを独自にレイアウトしたものを実装する方法を紹介していきます。この方法は専用のレイアウトファイル(xml)を用意して表示させる流れになります。
実装の手順
- DialogFragmentを継承したFragmentクラスの作成
- 独自レイアウトのXMLファイルを準備
- onCreateDialogメソッドをオーバーライドしてダイアログ構築処理を実装
- builder.create()してDialogインスタンスを取得しreturn
1.DialogFragmentを継承したFragmentクラスの作成
まずはDialogFragmentを継承したFragmentクラスを新規で作成しておきます。関連したレイアウトファイルも一緒に新規作成しておけばOKです。
class CustomNotifyDialogFragment: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
}
}
2.独自レイアウトのXMLファイルを準備
自動追加されたレイアウトファイルの中身を編集し、表示させたい独自レイアウトを実装ていきます。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="300dp"
android:layout_height="220dp"
android:background="@color/ex_thema">
<TextView
android:id="@+id/dialog_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ex_red"
android:padding="12dp"
android:text="@string/dialog_title_notice"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="16dp"
app:layout_constraintBottom_toTopOf="@+id/dialog_msg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/dialog_msg"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="messagemessagemessagemessagemessagemessagemessage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_title" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="172dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline2">
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/negative_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="NG" />
<Button
android:id="@+id/positive_button"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="OK" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
3.onCreateDialogメソッドをオーバーライドしてダイアログ構築処理を実装
作成したカスタムダイアログクラスのonCreateDialog
メソッド内でレイアウトファイルを参照してビューを構築していきます。
fragment_custom_notify_dialog
レイアウトをインフレートしsetView
メソッドでAlertDialog.Builder
インスタンスに渡します。またレイアウトの中のビューにはinflate
で取得したViewから参照することが可能です。
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(this.requireContext())
val inflater = this.requireActivity().layoutInflater
val dialog = inflater.inflate(R.layout.fragment_custom_notify_dialog,null)
val button: Button = dialog.findViewById(R.id.positive_button)
val title: TextView = dialog.findViewById(R.id.dialog_title)
title.text = "タイトル"
val msg: TextView = dialog.findViewById(R.id.dialog_msg)
msg.text = "メッセージ"
button.setOnClickListener {
dismiss()
}
builder.setView(dialog)
return builder.create()
}
DialogFragmentのライフサイクルはonCreateDialog
が呼ばれonViewCreated
などは呼ばれないので注意してください。
最後にActivityクラスなどからカスタムダイアログクラスをインスタンス化しshow
メソッドを呼び出すことで実装できます。
val button:Button = findViewById(R.id.done_button)
button.setOnClickListener{
val dialog = CustomNotifyDialogFragment()
dialog.show(supportFragmentManager, "custom")
}

Activityなどからデータを渡して表示する
ダイアログに表示させたいデータは他のActivityなどから受け取ったデータにしたいことのが多いと思います。外部から受け取ったデータを反映させたい場合はFragmentへのデータ渡しの方法と同じでコンストラクタの引数ではクラッシュしてしまうのでBundleを介してデータを渡します。詳細な実装方法はFragment側の以下の記事を参考にしてください。
実装の流れ
- キーを準備
- データ格納用のプロパティを用意
- コンストラクタではないインスタンス生成用メソッドの準備
- onCreate内でBundleからデータを取得してプロパティにセット
// ⓵キーを準備
public const val ARG_DIALOG_TITLE_KEY = "ARG_DIALOG_TITLE_KEY"
public const val ARG_DIALOG_MSG_KEY = "ARG_DIALOG_MSG_KEY"
class CustomNotifyDialogFragment : DialogFragment() {
// ⓶データ格納用のプロパティを用意
private var title: String = ""
private var msg: String = ""
// ⓷コンストラクタではないインスタンス生成用メソッドの準備
// ここでデータを外部から引数で受け取りBundleに保存する
companion object {
@JvmStatic
public fun newInstance(title: String, msg: String) =
CustomNotifyDialogFragment().apply {
arguments = Bundle().apply {
putString(ARG_DIALOG_TITLE_KEY, title)
putString(ARG_DIALOG_MSG_KEY, msg)
}
}
}
// ⓸onCreate内でBundleからデータを取得してプロパティにセット
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
title = it.getString(ARG_DIALOG_TITLE_KEY).toString()
msg = it.getString(ARG_DIALOG_MSG_KEY).toString()
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(this.requireContext())
val inflater = this.requireActivity().layoutInflater
val dialog = inflater.inflate(R.layout.fragment_custom_notify_dialog,null)
val button: Button = dialog.findViewById(R.id.positive_button)
val title: TextView = dialog.findViewById(R.id.dialog_title)
title.text = this.title
val msg: TextView = dialog.findViewById(R.id.dialog_msg)
msg.text = this.msg
button.setOnClickListener {
dismiss()
}
builder.setView(dialog)
return builder.create()
}
}
ダイアログを表示させる際はnewInstance
メソッドでインスタンスを生成して使用します。
val button:Button = findViewById(R.id.done_button)
button.setOnClickListener{
val dialog = CustomNotifyDialogFragment.newInstance("お知らせ", "メッセージ")
dialog.show(parentFragmentManager, "custom")
}
Activityなどから処理を渡して実行する
ダイアログのボタン押下時の処理を外部から渡せるようにしておけば、汎用的なカスタムダイアログクラスになり流用しやすくなります。外部から処理を渡すためにはDialogとActivityに依存関係が生じないように注意を払う必要があります。そのためDialog⇄Activity
とならないようにDialog⇄listener⇄Activity
とすることで依存し合わないようにしています。
以下の記事でFragment⇄listener⇄Activity
のやり取りを解説しているので参考にしてください。
実装の流れ
- リスナー本体を定義
- リスナーをlateinitプロパティとして定義
- リスナープロパティにセットするメソッドを用意
- 外部から渡された処理を実行したい箇所で呼び出す
class CustomNotifyDialogFragment : DialogFragment() {
// 〜〜〜〜 省略 〜〜〜〜
// ⓶リスナーをlateinitプロパティとして定義
private lateinit var listner: onTappedListner
// ⓵リスナー本体を定義
interface onTappedListner {
fun onTapped()
}
// ⓷リスナープロパティにセットするメソッドを用意
// これは外部から呼び出す
public fun setOnTappedListner(listener: onTappedListner) {
this.listner = listener
}
// 〜〜〜〜 省略 〜〜〜〜
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// 〜〜〜〜 省略 〜〜〜〜
button.setOnClickListener {
// ⓸外部から渡された処理を実行したい箇所で呼び出す
listner.onTapped()
dismiss()
}
builder.setView(dialog)
return builder.create()
}
}
Activityから処理を渡すには定義したsetOnTappedListner
メソッドを呼び出し、引数にタップ時に実行したい処理を組み込んだonTappedListner
型を渡せばOKです。
val button:Button = findViewById(R.id.done_button)
button.setOnClickListener{
val dialog = CustomNotifyDialogFragment.newInstance("お知らせ", "メッセージ")
dialog.setOnTappedListner(
object :CustomNotifyDialogFragment.onTappedListner{
override fun onTapped() {
Log.e("------","タップしたよ")
}
}
)
dialog.show(parentFragmentManager, "custom")
}
ダイアログ以外の部分のViewをタップを禁止する
ダイアログ表示している場合はデフォルトでは背景のViewをタップされた時にダイアログが閉じてしまうようになっています。これを防ぐにはsetCanceledOnTouchOutside
でタップ自体を禁止し、念の為isCancelable
もfalse
にしておくと良いです。
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// ダイアログ以外のタッチを禁止
dialog?.setCanceledOnTouchOutside(false)
// ダイアログの外をタップしてもキャンセルされないようにする
isCancelable = false
}
同じタグのダイアログを出さないようにする
タグで表示しているダイアログを識別することができるので、連続でボタンを押された時などに同じダイアログが表示されないようにダイアログを表示しているかチェックしてから表示することで重複したダイアログを表示しないようにすることができます。そのためにはsupportFragmentManager
からfindFragmentByTag
を使用して対象のDialogFragment
があるかどうかを確認します。
// 同じタグのものが表示されていないかチェックする
if ((supportFragmentManager.findFragmentByTag(tag) as? DialogFragment) == null) {
val dialog = CustomNotifyDialogFragment.newInstance(
title = getString(R.string.dialog_title),
msg = getString(R.string.dialog_msg),
showPositive = true,
showNegative = false
)
dialog.show(supportFragmentManager, tag)
}
この場合にshow
メソッド表示させていると高速で複数回呼び出された時などに対応できず、重複して表示されてしまうことがあります。その場合はshowNow
メソッドを使用すると改善します。
showとshowNowの違いと使い方
DialogFragment
を表示するメソッドにはshow
とshowNow
メソッドがあります。定義を確認してみるとcommit
とcommitNow
の違いのようです。
showメソッド
show
メソッドは非同期的にDialogFragment
を表示。フラグメントトランザクションがコミットされたあと、次のUIスレッドの更新タイミングで表示される。
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.setReorderingAllowed(true);
ft.add(this, tag);
ft.commit();
}
showNowメソッド
showNow
メソッドは同期的にDialogFragment
を表示。フラグメントトランザクションがコミットされ即座に表示される。
public void showNow(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.setReorderingAllowed(true);
ft.add(this, tag);
ft.commitNow();
}
例えば以下のように流れで3つの同じフラグメントを表示する際にshowNow
だと、1番目しか表示されませんが、show
だと全て表示されてしまいます。つまり同じカスタムダイアログが表示されないようにするためにはshowNow
を使用した方が良さそうです。
var tag = "FailedDialog"
if ((supportFragmentManager.findFragmentByTag(tag) as? DialogFragment) == null) {
val dialog = CustomNotifyDialogFragment.newInstance(
title = getString(R.string.dialog_title_notice),
msg = "1番目",
showPositive = true,
showNegative = false
)
dialog.showNow(supportFragmentManager, tag)
}
if ((supportFragmentManager.findFragmentByTag(tag) as? DialogFragment) == null) {
val dialog2 = CustomNotifyDialogFragment.newInstance(
title = getString(R.string.dialog_title_notice),
msg = "2番目",
showPositive = true,
showNegative = false
)
dialog2.showNow(supportFragmentManager, tag)
}
if ((supportFragmentManager.findFragmentByTag(tag) as? DialogFragment) == null) {
val dialog3 = CustomNotifyDialogFragment.newInstance(
title = getString(R.string.dialog_title_notice),
msg = "3番目",
showPositive = true,
showNegative = false
)
dialog3.showNow(supportFragmentManager, tag)
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。