AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

Clean ArchitecutureにおけるKotlin coroutinesの処理と責務分け

自分の中で混乱してきたので整理する。

環境

  • Kotlin 1.2.71
  • Kotlin Coroutines 0.30.1

本題

以下のようなClean Architecuteベースの設計のアプリがあるとする。

f:id:phicdy:20181011225951p:plain

このとき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になる。また破壊的変更が来ないことを願う。