こんにちは。最近こっそりUnity勉強中の岩下です。

前回は簡単な例を紹介しましたが、今回はCleanRecordsにおけるダイアログの表示方法について紹介したいと思います。

キーワード:AndroidClean ArchitectureKotlinDialogFragment

今回のテーマ

今回は、下記2つのダイアログを例に用いて、CleanRecordsにおけるダイアログの表示方法を紹介します。

保存完了ダイアログ

ユーザが生徒を追加・編集後保存した際に、保存が成功したことをユーザに知らせるためのダイアログです。

saved

関連するユースケースは下記のような感じです。

  • アプリは生徒追加・編集画面を表示する。
  • ユーザは生徒情報を編集し、「SAVE」ボタンを押下する。
  • アプリは生徒情報をDBに保存する。
  • アプリは「保存が成功したことを示すメッセージ」「OK」を含むダイアログを表示する。
  • ユーザは「OK」を押下する。
  • アプリは生徒一覧画面を表示する。

削除確認ダイアログ

ユーザが生徒を削除しようとした際に、実際に削除をする前にユーザに確認を取るためのダイアログです。

delete

関連するユースケースは下記のような感じです。

  • アプリは生徒追加・編集画面を表示する。
  • ユーザは「DELETE」ボタンを押下する。
  • アプリは「対象の生徒を本当に消してよいかを確認するメッセージ」「CANCEL」「OK」を含むダイアログを表示する。
  • ユーザが「CANCEL」を押下した場合、アプリはダイアログを消す。
  • ユーザが「OK」を押下した場合、アプリは対象の生徒をDBから削除する。アプリは生徒一覧画面を表示する。

DialogFragmentを使う

Androidでダイアログを表示する場合は、公式に推奨されている通りDialogFragmentを使用します。ここで、Clean ArchitectureにおいてDialogFragmentを実装しようとすると、以下の2パターンが考えられると思います。

  1. DialogFragmentもFragmentであるため1つのSceneとする
  2. ダイアログはあくまでViewとみなして同じScene内に配置する

Clean Architecture的には、やはり 1. にすべきと考えられます。とはいえ、すべてのダイアログを別Sceneで作るのはやりすぎな気もしますし、単純なダイアログでは 2. でも良いかと思います。CleanStoreでは、「ロジックが存在するかどうか」を判断基準の1つとし、ケースバイケースで 1. 2. を使い分けるようにしています。

なお、ロジックが複雑な場合には、そもそも本当にダイアログで実現する画面なのかどうかを考え直した方が良いと思います。
(実際に、DialogFragmentでは複雑な画面遷移に対応しきれず、途中で泣く泣くFragmentに替えた経験があります。)

今回は、

  • 保存完了ダイアログ: 2.
  • 削除確認ダイアログ: 1.

で実装しており、それぞれについて実装例を紹介します。

1つのSceneとする場合( 1. のパターン)

通常のSceneは Fragment Interactor Presenter Router Models の5つから構成されますが、このケースでは FragmentDialogFragment に置き換えます。 Fragment の場合と書き方が異なってくるのは Fragment(DialogFragment) と 呼び出し側の Router のみです。

Fragment(DialogFragment)の差異

DialogFragmentを使う場合はFragmentを使う場合に対して以下の点が異なってきます。

  • Fragment ではなく DialogFragment を継承する。
  • ライフサイクルが若干異なる。
    • onViewCreated() は呼ばれないため使わない。
    • Viewのsetupは onCreateDialog() で実施する。
    • 画面表示時に発火すべきユースケースは onCreateView() で発火する。

下記はDeleteStudentDialogFragmentのソースの一部です。ダイアログ表示時に、メッセージに表示する生徒名「たなか」を取得するユースケース(getStudent())を発火し、その結果でsetMessage()によりメッセージを更新しています。また、「OK」押下時には、生徒を削除するユースケース(deleteStudent())を発火し、その結果で生徒一覧画面へ遷移するRoutingLogic(routeToListStudents())を呼んでいます。

scenes/deletestudent/DeleteStudentDialogFragment.kt

class DeleteStudentDialogFragment : DialogFragment(), DeleteStudentDisplayLogic {
  ...

  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    isCancelable = false  // Dialog外タップ・BackでDialogを消さない

    return AlertDialog.Builder(requireContext())
      .setPositiveButton(R.string.ok) { _, _ ->
        deleteStudent()  // OKボタン押下時に生徒削除ユースケース発火
      }
      .setNegativeButton(R.string.cancel, null)
      .create()
  }

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    getStudent()  // 画面(ダイアログ)表示時に生徒の名前取得ユースケース発火

    return super.onCreateView(inflater, container, savedInstanceState)
  }

  // MARK: - GetStudent

  private fun getStudent() {
    val request = DeleteStudentModel.GetStudent.Request()
    interactor.getStudent(request)
  }

  override fun displayStudent(viewModel: DeleteStudentModel.GetStudent.ViewModel) {
    (this.dialog as AlertDialog).setMessage(getString(R.string.message_confirm_delete_student, viewModel.name))  // ダイアログのメッセージを更新(生徒名を反映)
  }

  // MARK: - DeleteStudent

  private fun deleteStudent() {
    val request = DeleteStudentModel.DeleteStudent.Request()
    interactor.deleteStudent(request)
  }

  override fun displayDeleteStudent(viewModel: DeleteStudentModel.DeleteStudent.ViewModel) {
    router.routeToListStudents()
  }
}

Routerの差異

当たり前ですがFragmentとDialogFragmentでは表示の方法が異なるため、呼び出し側のRouterのメソッドの記述が変わってきます。具体的には、前者ではFragmentTransactionを使った方法、後者ではDialogFragment#show()を使った方法になります。CreateStudentFragmentからDeleteStudentDialogFragmentを表示する処理は下記のようになっています。

scenes/createstudent/CreateStudentRouter.kt

  override fun routeToDeleteStudent() {
    val student = dataStore.student ?: return

    DeleteStudentDialogFragment().apply {
      router.dataStore.student = student
    }.show(fragment.fragmentManager, null)
  }

同Sceneに配置する場合( 2. のパターン)

次に、同Sceneに配置する場合です。この場合は、Clean Architectureはもはや関係ありません。DialogFragmentの定義を、表示するFragmentと同じファイル内に記述します。今回の例では、CreateStudentFragment.kt内にCreateStudentFragmentとCreateStudentSavedDialogFragmentの2つのクラスがある状態になります。CreatFragmentクラスのインナークラスとして定義したい気持ちもありますが、DialogFragmentクラスは非インナー・Publicで定義しなければエラーになるためこのようにしています。

以下、ソースです。

scenes/createstudent/CreateStudentFragment.kt

class CreateStudentFragment : CreateStudentSavedDialogFragment.Listener, ... {
  ...

  // 保存が完了したら呼ばれる
  override fun displayUpdateStudent(viewModel: CreateStudent.UpdateStudent.ViewModel) {
    CreateStudentSavedDialogFragment.show(requireFragmentManager(), this)
  }

  // CreateStudentSavedDialogFragmentのListenerメソッド
  override fun onSavedDialogOkClick() {
    router.routeToListStudents()
  }
}

class CreateStudentSavedDialogFragment : DialogFragment() {
  interface Listener {
    fun onSavedDialogOkClick()
  }

  companion object {
    fun show(fragmentManager: FragmentManager, listener: Listener) {
      CreateStudentSavedDialogFragment().apply {
        this.listener = listener
      }.show(fragmentManager, null)
    }
  }

  private lateinit var listener: Listener

  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    isCancelable = false

    return AlertDialog.Builder(requireContext())
      .setMessage(R.string.message_save_success)
      .setPositiveButton(R.string.ok) { _, _ ->
        listener.onSavedDialogOkClick()
      }
      .create()
  }
}
  • interface Listener は呼び出し元のFragmentに通知するためのInterfaceです。呼び出し側のCreateStudentFragmentは当Interfaceを実装しています。
  • クラスメソッドの show でDialogFragmentを生成・showします。引数にListenerを受け取り、「OK」ボタン押下時にそのメソッド(onSavedDialogOkClick())を呼ぶことでCreateStudentFragmentに通知を行っています。CreateStudentFragmentはそのメソッドの中でrouterを発火し画面遷移処理を行っています。

おわりに

今回はCleanRecordsにおけるDialogFragmentの表示方法を2通り紹介しました。正直なところ、私もどちらを採用すべきかの線引きがはっきりとできていない状態ですが、複数人で開発に当たる場合はその辺りの方針を事前にしっかりと決めておきたいところですね。と締めくくりながら、やっぱり全部パターン1で書いた方がいいんじゃないかと思い直したりもしています(笑)

Clean Architureとうまく付き合っていくために、本記事が何かのヒントになれば幸いです。

今回説明に用いたソースコードはこちらにありますのでよろしければご覧ください。

前回 → ②〜簡単なユースケース例〜
次回 → ④〜エラーを扱う〜

© 2018. SuperSoftware Co., Ltd. All Rights Reserved.