こんにちは。最近お腹の肉の肥大化が気になってきている岩下です。

今回はCleanRecordsにおけるエラーの取り扱いについて紹介したいと思います。

キーワード:AndroidClean ArchitectureKotlin

今回のテーマ

Clean Architectureに関係なく、エラーの実装方法は常々一つの悩みどころになってくると思います。今回は下図のような簡単な例を用いて、CleanRecordsでのエラー実装について紹介します。

records

上図は「生徒の成績を表示する」画面です。通常時は生徒の成績を表示しますが、エラー時には成績の代わりにトーストを表示するようになっています。

早速実装について見ていきましょう。

実装

Provider & Models

まず、「生徒の成績を取得する」Providerは下記のようにしています。

providers/RecordsProvider.kt

interface RecordsStoreInterface {
  fun fetchRecords(studentId: String, completionHandler: (Records?, Error?) -> Unit)
}

class RecordsProvider(private val recordsStore: RecordsStoreInterface) {
  fun fetchRecords(studentId: String, completionHandler: (Records?, Error?) -> Unit) {
    recordsStore.fetchRecords(studentId, completionHandler)
  }
}

この中でエラーに関連する部分は、 completionHandler の引数です。 Records および Error はOptionalにしておき、通常時はErrorがnullに、エラー時はRecordsがnullになるように、Service側で値をセットするようにします。

Kotlinは関数に複数の戻り値を持つことができませんが、ラムダ式を使うことで2つの値をProviderの呼び元(Interactor)への通知を実現しています。ラムダ式は便利ですね!

ここで、 Error はエラーの種類を表す enum class で、models配下に定義しています。

models/Error.kt

enum class Error {
  NOT_FOUND,
  UNKNOWN;

  companion object {
    fun from(name: String): Error? {
      return Error.values().find { it.name == name }
    }
  }
}

Interactor (BusinessLogic) & Models

次にInteractorです。先ほどのRecordsProvider.fetchRecords()をコールし、第2引数で records, error を受け、それらをそのままResponseに詰めてPresenterに渡しています。

scenes/showrecords/ShowRecordsInteractor.kt

...
  override fun fetchRecords(request: ShowRecords.FetchRecords.Request) {
    val student = this.student

    recordsProvider.fetchRecords(student.studentId) { records, error ->
      val response = ShowRecords.FetchRecords.Response(
        records,
        student,
        error
      )
      presenter.presentRecords(response)
    }
  }
...

ここで、Responseを含むFetchRecordsのModelは次のようになっています。

scenes/showrecords/ShowRecordsModels.kt

object ShowRecords {
  object FetchRecords {
    class Request
    data class Response(
      val records: Records?,
      val student: Student,
      val error: Error?
    )
    data class ViewModel(
      val name: String,
      val records: RecordsViewModel?,
      val errorMessageRId: Int?
    )
  }

  data class RecordsViewModel(
    val totalScore: String,
    val englishScore: String,
    val mathScore: String,
    val scienceScore: String
  )
}

Providerの場合と同様に、Response内のRecordsとErrorはともにOptionalにしておき、通常時・エラー時にいずれかをnullとする形としています。

Presenter (PresentationLogic)

PresenterはInteractorからResponseを受け取り、 response.error がnullかどうかで通常時・エラー時のViewModelを作り分けています。今回は簡単な例のため、1つのViewModel内にエラーメッセージを表すResourceIDを含める形にしていますが、より複雑な場合にはエラー用のViewModel(ErrorViewModel)とそれに対応するDisplayLogicを別で用意し、それらをPresenterで呼び分けるといった形も考えられます。

scenes/showrecords/ShowRecordsPresenter.kt

...
  override fun presentRecords(response: ShowRecords.FetchRecords.Response) {
    val name = response.student.name

    val viewModel = response.error?.let {  // エラー時
      ShowRecords.FetchRecords.ViewModel(
        name,
        null,
        R.string.message_error_get_records
      )
    } ?: run {  // 通常時
      val records = response.records

      val totalScore = (records?.englishScore?.let { it } ?: 0)
        .plus(records?.mathScore?.let { it } ?: 0)
        .plus(records?.scienceScore?.let { it } ?: 0)

      val recordsViewModel =ShowRecords.RecordsViewModel(
        totalScore.toString(),
        records?.englishScore?.toString() ?: "",
        records?.mathScore?.toString() ?: "",
        records?.scienceScore?.toString() ?: ""
      )

      ShowRecords.FetchRecords.ViewModel(
        name,
        recordsViewModel,
        null
      )
    }

    fragment.displayRecords(viewModel)
  }
...

Fragment (DisplayLogic)

最後に、FragmentではViewModel内の各値がnullかどうかを見て、成績の表示およびエラートーストの表示を行っています。ここで、DisplayLogicは errorMessage がnullかどうかを見て成績表示・エラー表示を出し分けるということをせず、あくまでそれぞれがnullかどうかを見てそれぞれを別個に表示しています。これは、DisplayLogicは「エラー時かどうか」の状態は把握していないべきであり、Presenterから指示された表示処理を行うのみであるべき、というルールに則っているためです。こうしておくことで、例えばエラー時に特定の成績を表示するように仕様変更が発生した場合にも、DisplayLogicは一切手を加えずに済みます。

scenes/showrecords/ShowRecordsFragment.kt

  override fun displayRecords(viewModel: ShowRecords.FetchRecords.ViewModel) {
    hisScoreTextView.text = getString(R.string.his_total_score, viewModel.name)

    viewModel.records?.let {  // 通常時は成績表示
      totalScoreTextView.text = it.totalScore
      englishScoreTextView.text = it.englishScore
      mathScoreTextView.text = it.mathScore
      scienceScoreTextView.text = it.scienceScore
    }

    viewModel.errorMessageRId?.let {  // エラー時はトースト表示
      Toast.makeText(context, it, Toast.LENGTH_LONG).show()
    }
  }

おわりに

簡単でしたが、今回はCleanRecordsのエラーの扱いについて紹介しました。エラー用のModelを用意するなど色々試してみましたが、今回の方法が一番スッキリまとまった印象です。皆さんも色々と試してみてください。

CleanRecordsのソースコードはこちら

前回 → ③〜ダイアログを表示する〜

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