Razl-Dazl

eyecatch
Copyright © Google

DialogFragmentで出来るだけシンプルにコールバックを実装する

Posted at — 2023-07-26

Fragment系は空のコンストラクタにアクセスできないとActivity再生成時に詰むので、色々細工するかと思いますが、コールバックの実装って結構ややこしいですよね・・・

仕様についてうまく割り切れば比較的シンプルに実装できたのでここに残しておきます


実装の都合から仕様を決める

前提として、Fragmentに値を渡したい時はコンストラクタの引数に直接入れず、インスタンスを作ってからプロパティにセットするのがお作法ですよね

class SimpleDialog: DialogFragment() {  
    private var listener: SimpleDialogListener? = null

	...

	companion object {
		@JvmStatic  
		fun newInstance(listener: SimpleDialogListener, title: String): SimpleDialog {  
			val arg = Bundle().apply {  
				putString("title", title)  
			}  
			return SimpleDialog().apply {  
				this.arguments = arg  
				this.listener = listener  
			}  
		}  
	}
	
	...
	
	interface SimpleDialogListener {
		...
	}
}

で、このやり方だと、画面回転が行われる等してActivityの再生成が発生した場合に、listenerはnullになってしまいますよね

その状態になってしまうと、ダイアログ上で「はい」や「キャンセル」を押してもlistenerのメソッドが呼び出されません

ここで無理やりlistenerを取得するには、onAttach()からContextを参照することによって (Context as SimpleDialogListener)とキャストするような方法も考えられますが・・・

Fragmentから呼び出している場合や匿名クラスを引数にとる場合、この方法は使えませんので別の手を考える必要があります

条件分岐が増えて見通しが悪くなりそうですね・・・

では、いっそのことダイアログを閉じてしまうというのは如何でしょうか?

override fun onStart() {  
    super.onStart()  
    if(listener == null) dialog?.cancel()  
}

onStartでlistenerの存在をチェックします

最初のインスタンス生成時は、newInstance()でlistenerへの代入が行われるので、ここのif文はスキップされます

Activityの再生成が発生した時は、再度onStart()が呼び出された際にはlistenerがnullになっているはずです

ここでダイアログを閉じてしまいます

こうすることによって「あれ、押しても何も起こらないぞ・・・」を防ぐことが出来ます


コード全体

class SimpleDialog: DialogFragment() {  
    private var listener: SimpleDialogListener? = null  
  
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {  
        return AlertDialog.Builder(activity).apply {  
            val title = requireArguments().getString("title", "empty")  
            setTitle(title)  
  
            setPositiveButton(getString(R.string.dialog_yes), DialogInterface.OnClickListener { _, _ ->  
                listener?.onDialogAccept()  
            })  
            setNeutralButton(getString(R.string.dialog_cancel), DialogInterface.OnClickListener { _, _ ->  
                listener?.onDialogCancel()  
            })  
        }.create()  
    }  
  
    override fun onStart() {  
        super.onStart()  
        if(listener == null) dialog?.cancel()  
    }  
   
    override fun onCancel(dialog: DialogInterface) {  
        super.onCancel(dialog)  
        listener?.onDialogCancel()  
    }  
  
    companion object {  
        @JvmStatic  
        fun newInstance(listener: SimpleDialogListener, title: String): SimpleDialog {  
            val arg = Bundle().apply {  
                putString("title", title)  
            }  
            return SimpleDialog().apply {  
                this.arguments = arg  
                this.listener = listener  
            }  
        }  
    }  
  
    interface SimpleDialogListener {  
        fun onDialogAccept()  
        fun onDialogCancel()  
    }  
}

ウォ~結構シンプルになった

ちなみにonCancel()でもリスナクラスのメソッドを呼び出すようにしています

理由としては ダイアログの領域外をタッチした時とかの扱いを、キャンセル押下時の扱いと同じにする為 になります

ダイアログを表示する際にはnewInstance()から行うことをお忘れなく~

SimpleDialog.newInstance(listener, getString(R.string.dialog_title))  
    .show(supportFragmentManager, "example")

この実装方法の長所

  • コードの量が少なくて済む
  • リスナのインターフェースをどこに実装しても(Activity、Fragment、匿名クラスのどれでも)同じ使い方ができる

短所

  • ダイアログを表示し続けるのは諦める必要がある

おしまい

Author@zakuro

Mastodon: 396@vivaldi.net