Clean ArchitecutureにおけるKotlin coroutinesの処理と責務分け
自分の中で混乱してきたので整理する。
環境
- Kotlin 1.2.71
- Kotlin Coroutines 0.30.1
本題
以下のようなClean Architecuteベースの設計のアプリがあるとする。
このときKotlin Coroutineを使って非同期処理を行ってUIを更新するとする。 各クラスの責務を分けると以下のようになる。
クラス | 役割 | やらない・知らない | スレッド |
---|---|---|---|
Activity/Fragment | UIを更新する。インターフェースをPresenterに提供し、イベントの処理をPresenterに移譲する。 | UIロジックを持たない。Presenterが何をするかは知らない | UIスレッド |
Presenter | UseCaseに処理を依頼し処理結果を待つ。その結果に応じてUIを更新する | UIの実態を知らない。UseCaseがどう処理を実行しているか知らない | UIスレッド |
UseCase | Repository/Apiからデータを取得して処理を行う | Repository/Apiがどこからデータを取得しているか知らない。 | UIスレッド/ワーカースレッド |
Repository/Api | 外部・内部からデータを取得する | 返したデータがどう使われるかは知らない | ワーカースレッド |
PresenterはUseCaseがどう処理を実行するか知らないし、UseCaseもRepository/Apiがどう処理をするかを知らない。めちゃくちゃ早いCPUで処理をメインスレッド上で5秒以内に実行しているかもしれないし、ワーカースレッドで待ち合わせをして結果を返しているかもしれない。現実的にはUseCaseがRepository/Apiの結果を待ち合わせをし、処理を行って結果を返す。
Activity/FragmentはAndroid SDKに基づくUIスレッドで実行するコルーチンビルダーを呼ぶ。
class MyActivity: AppCompatActivity(), MyView, CoroutineScope { private lateinit var presenter: MyPresenter private val job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main override fun onCreate() { presenter = MyPresenter(this) launch(context = coroutineContext) { presenter.onCreate() } } override fun showSuccess() { ... } override fun showFail() { ... } }
Presenterでは呼び出し元のCoroutineScopeで処理を実行し、結果に応じてUIを更新する。UseCaseがどのスレッドで処理をしているかは意識しない。
class MyPresenter(private view: MyView) { suspend fun onCreate() = corountineScope { val result = useCase.fetchAndExecute() if (result.isSuccess()) { view.showSuccess() } else { view.showFail() } } }
UseCaseは中でRepositoryからデータを取得する。Repositoryがどのスレッドで処理をしているかは意識しない。呼び出し側のUseCaseは非同期を意識せずに同期的に書くことができる。返ってきた結果を別メソッドで処理するが、ここでもスレッドを意識しない。実際には withContext()
でワーカースレッド上で処理される。
class MyUseCase { suspend fun fetchAndExecute(): Result = corountineScope { val data = repository.fetchData() return onFetch(data) } private suspend fun onFetch(data: List<Model>): Result = coroutineScope { return@coroutineScope withContext(Dispatchers.Default) { // なんか重たい処理 return@withContext ... } } } }
Repository/Apiでは withContext()
でIOスレッドに切り替えて処理を返す。
class MyRepository { suspend fun fetchData(): List<Model> = coroutineScope { return@coroutineScope withContext(Dispatchers.IO) { // データベース等からなにかを取得する処理 return@withContext ... } } }
Presenterのテスト
テストではUseCaseとViewをモックし、 runBlocking
内で実行することでAndroid SDKからの依存をなくし、同期的に実行する。
@Test fun `when fetch and execute succeeds then show success`() = runblocking { val view = mock(MyView::class.java) val useCase = mock(MyUseCase::class.java) val presneter = MyPresneter(view) `when`(useCase.fetchAndExecute()).thenReturn(...) presenter.useCase = useCase presenter.onCreate() } verify(view, times(1)).showSuccess() }
おわりに
0.26.1の破壊的変更でいろいろ調べていたら時間がかかってしまった。もうそろそろKotlin 1.3が出てCoroutinesが1.0になる。また破壊的変更が来ないことを願う。