こんにちは。ついこの間まではKotlinにどっぷり浸かっていたのに、最近はゴリゴリPythonを書いている岩下です。Kotlinのありがたみを一度知ってしまった分、つらみが増しています。。

さて、今回は、前回に引き続き、簡単なユースケースを用いてCleanRecordsの説明を行いたいと思います。

キーワード:AndroidClean ArchitectureKotlin

ユースケース

今回説明に用いるユースケースは、「生徒一覧を取得して表示する」です。表示のイメージは下記になります(「EDIT」等のボタンは無視してください)。

seito_ichiran_wo_hyoji_suru

「生徒一覧を取得して表示する」の要件は下記のとおりです。

  • 生徒一覧のデータを任意の場所から取得する
  • 生徒の合計人数を表示する
  • 生徒をリスト表示する
    • 性別をアイコン表示する
    • 名前を表示する

このユースケースを実現するためのコードを順に説明します。

処理の流れ

ソースコードを見ていく前に、先に処理の流れの図を紹介しておきます。

Fetch-Students3

このように、一つのユースケースについて、FragmentからFragmentへ一周するような流れになります。以降でそれぞれのソースについて見ていきましょう。

ソースコード

Model

まずは、「生徒」のEntityが必要なため、Student Modelを定義します。

models/Student.kt


data class Student(
  val id: String,
  var name: String,
  var gender: Gender?
)

enum class Gender {
  MALE,
  FEMALE;

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

生徒は「名前」と「性別」および管理用のIDを持っています。

性別に関しては「男性」「女性」のどちらかの値となるため、enum classを使って定義しています。後々、文字列("MALE" or "FEMALE")からGender classに変換する場面が出てくるため、そのためのメソッドも定義しています(もっとうまいやり方があるといいのですが。)

Provider / Service

次に、先に定義したStudentのリストを外部から取得して返すためのInterfaceをProviderに定義します。

providers/StudentProvider.kt

interface StudentStoreInterface {
  fun fetchStudents(): List<Student>
}

class StudentProvider(private val studentStore: StudentStoreInterface) {
  fun fetchStudents(): List<Student> {
    return studentStore.fetchStudents()
  }
}

そして、このStudentStoreInterfaceを実装したServiceを定義します。これは、Serviceの種類によってStudentMemStoreだったりStudentWebApiだったりStudentRealmだったりします。今回は、Realmを用いた例です。

services/StudentRealm.kt

open class StudentObject(
  @PrimaryKey open var id: String = "",
  open var name: String = "",
  open var gender: String? = null
) : RealmObject() {
  val student: Student
    get() = Student(
      id,
      name,
      gender?.let { Gender.from(it) }
    )
}

class StudentRealmStore : StudentStoreInterface {
  override fun fetchStudents(): List<Student> {
    return Realm.getDefaultInstance().use {
      it.where(StudentObject::class.java)
        .findAll()
        .map { it.student }
    }
  }
}

RealmはRealmで扱えるようにするためにRealmObjectを継承したデータモデルを定義してやる必要があります。このRealmObjectはService(Realm)に依存したものになりますので、Serviceの中に定義してやります。

StudentRealmStoreがStudentStoreInterfaceを実装しており、実際にRealm Serviceを用いてデータを取得・返却する処理を行っています。

ここまでで、外部から生徒一覧を取得する部分が完成しました。

Scene

Sceneとしては「生徒一覧を表示するScene」となりますので、scenes配下に「liststudents」パッケージを設置し、その中にFragment・Interactor・Models・Presenter・Routerを配置しています。

Models

次に、「生徒一覧を表示するScene」に「生徒一覧を表示する」ユースケースを実現するためのModelを定義します。

object ListStudents {
  object FetchStudents {
    class Request
    data class Response(
      val students: List<Student>
    )
    data class ViewModel(
      val totalCount: String,
      val students: List<StudentViewModel>
    )
  }

  data class StudentViewModel(
    val id: String,
    val iconRId: Int,
    val name: String
  )
}

ここで、それぞれ

  • Request: FragmentがBusinessLogicに渡すデータ型
  • Response: InteractorがPresentationLogicに渡すデータ型
  • ViewModel: PresenterがDisplayLogicにわたすデータ型

であり、表示用のStudentとしてStudentViewModelも併せて定義しています。性別のアイコンはリソースから取得する固定画像のため、リソースID iconRId を渡すデータとしています。

なお、「生徒一覧を表示する」Requestにデータは不要ですが、使用側の記述の統一化のために空のRequest classを定義しています(data classはコンストラクタに1つ以上値が必要なため、ただのclassとしています)。

Fragment

次に、少し長いですがFragmentのソースです。

interface ListStudentsDisplayLogic {
  fun displayStudents(viewModel: ListStudents.FetchStudents.ViewModel)
}

class ListStudentsFragment : Fragment(), ListStudentsDisplayLogic {
  private lateinit var interactor: ListStudentsBusinessLogic
  private lateinit var router: ListStudentsRouterInterface

  private lateinit var totalTextView: TextView
  private lateinit var recyclerView: RecyclerView

  // MARK: - Constructor

  init {
    setup()
  }

  // MARK: - Fragment

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                            savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)

    return inflater.inflate(R.layout.fragment_list_students, container, false)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    setupViews(view)

    fetchStudents()
  }

  // MARK: - Private

  private fun setup() {
    val interactor = ListStudentsInteractor()
    val presenter = ListStudentsPresenter()
    val router = ListStudentsRouter()

    this.interactor = interactor
    this.router = router
    interactor.presenter = presenter
    presenter.fragment = this
    router.fragment = this
    router.dataStore = interactor
  }

  private fun setupViews(view: View) {
    totalTextView = view.findViewById(R.id.text_view_total) as TextView

    recyclerView = (view.findViewById(R.id.recycler_view) as RecyclerView).apply {
      layoutManager = LinearLayoutManager(context)
      addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
    }
  }

  // MARK: - FetchStudents

  private fun fetchStudents() {
    val request = ListStudents.FetchStudents.Request()
    interactor.fetchStudents(request)
  }

  override fun displayStudents(viewModel: ListStudents.FetchStudents.ViewModel) {
    totalTextView.text = viewModel.totalCount
    recyclerView.adapter = StudentAdapter(viewModel.students)
  }

  // MARK: - RecyclerView

  inner class StudentViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    var genderImageView: ImageView = view.findViewById(R.id.image_view_gender) as ImageView
    var nameTextView: TextView = view.findViewById(R.id.text_view_name) as TextView
    var editButton: Button = (view.findViewById(R.id.button_edit) as Button)
    var recordButton: Button = (view.findViewById(R.id.button_record) as Button)
  }

  inner class StudentAdapter(private val students: List<ListStudents.StudentViewModel>)
      : RecyclerView.Adapter<StudentViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder {
      val inflater = LayoutInflater.from(parent.context)
      val view = inflater.inflate(R.layout.list_item_student, parent, false)

      return StudentViewHolder(view)
    }

    override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
      val student = students[position]

      holder.run {
        genderImageView.setImageResource(student.iconRId)
        nameTextView.text = student.name
      }
    }

    override fun getItemCount(): Int {
      return students.size
    }
  }
}

ListStudentsFragmentは大きく分けて下記のことをしています。

  1. Viewの定義、レイアウトXMLとの紐付け
  2. Scene内の各クラスのインスタンス化、依存性注入(setup())
  3. BusinessLogic発火(Request)メソッドの実装(fetchStudents())
  4. DisplayLogicの実装(displayStudents())
  5. RecyclerView用のViewHolderとAdapterを定義

3ではRequestを生成して該当するBusinessLogicを発火し、4ではViewModelを受けて該当するViewの更新を行っています。3と4は対になる(3の結果として4がコールされる)ため、わかりやすいようにコメントを入れて並べて記述しています。

Presenter

PresenterはInteractorから受け取ったResponseを、DisplayLogicがViewを更新できるような形式(ViewModel)に変換しています。

interface ListStudentsPresentationLogic {
  fun presentStudents(response: ListStudents.FetchStudents.Response)
}

class ListStudentsPresenter : ListStudentsPresentationLogic {
  lateinit var fragment: ListStudentsDisplayLogic

  override fun presentStudents(response: ListStudents.FetchStudents.Response) {
    val students = response.students.map {
      val iconRId = when (it.gender) {
        Gender.MALE -> R.drawable.male
        Gender.FEMALE -> R.drawable.female
        else -> R.drawable.unknown
      }

      ListStudents.StudentViewModel(
        it.studentId,
        iconRId,
        it.name
      )
    }

    val viewModel = ListStudents.FetchStudents.ViewModel(
      students.size.toString(),
      students
    )
    fragment.displayStudents(viewModel)
  }
}

Interactor

最後に、Interactorです。Interactorは、FragmentからRequestを受け、各Providerを経由してServiceの操作やDataStoreの操作を実施し、PresentationLogicにそのResponseを返します。

interface ListStudentsBusinessLogic {
  fun fetchStudents(request: ListStudents.FetchStudents.Request)
}

interface ListStudentsDataStore {
  var students: List<Student>?
}

class ListStudentsInteractor : ListStudentsBusinessLogic, ListStudentsDataStore {
  lateinit var presenter: ListStudentsPresentationLogic

  private val studentProvider = StudentProvider(StudentRealmStore())

  // MARK: - ListStudentsDataStore

  override var students: List<Student>? = null

  // MARK: - ListStudentsBusinessLogic

  override fun fetchStudents(request: ListStudents.FetchStudents.Request) {
    val students = studentProvider.fetchStudents()
    this.students = students

    val response = ListStudents.FetchStudents.Response(students)
    presenter.presentStudents(response)
  }
}

下記の部分で、ProviderおよびServiceのインスタンス化・依存性注入を行っています。

private val studentProvider = StudentProvider(StudentRealmStore())

BusinessLogic fetchStudents() では、Providerを経由してRealmから生徒一覧を取得し、それを内部のDataStoreに保存した後、生徒一覧をResponseに詰めて該当のPresentationLogicをキックしています。

おわりに

今回は「生徒一覧を取得して表示する」ユースケースにおいて、実現するための処理の流れとそれぞれの内部処理について紹介しました。本記事で、少しでもClean Architecture+Kotlinでの実装イメージを付けていただければ幸いです。今回は比較的普遍的なテーマを題材としましたが、次回からはAndroid固有の事例も紹介していければと思います。

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

前回 → ①〜構造について〜
次回 → ③〜ダイアログを表示する〜

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