今からAndroidにCoroutineを取り入れたときのアーキテクチャについて
まえがき
今年のDroidKaigiのアプリのソースコードが公開されました。
DroidKaigi/conference-app-2020: The Official Conference App for DroidKaigi 2020 Tokyo
Kotlin Multiplatform Projectで作らていて、KotlinでiOSとかもろもろ動かしちゃうのすごいですよね。
API通信とかでKotlin Coroutineが使用されています。さらにFlowを取り込んでおり去年までのCoroutineを使ったものと変わっています。
自分なりに、参考にしつつすごいシンプルなサンプルプロジェクトをつくりました。
サンプル
去年と今年ともDaggerの代わりにKoinを使用してあります。GitHubからユーザ名を検索して表示するだけのアプリです。
今年のバージョン:kamedon/GitHubSample2020
去年のバージョン:kamedon/GitHubSample: OkHttp 4 + Retrofit 2.6 + Coroutine + Koin Sample
比較すると大きくは以下の通り。大きなMVVM構成は変わっていなく、JetPack2.2.0でCoroutine対応が入ったことで扱いやすくなったのと、Flowが入ってきたのでストリームとして扱えるようになりました。
- Repository層がsuspend functionからFlowを返すようになったこと
- ViewModelでコルーチンを起動する際にwithContextからliveData{}, viewModelScope.launchで起動し、あえてバッググランドのスレッドをどこにするか指定しなくなったこと
Repository層がsuspend functionからFlowを返すようになったこと
去年
class GitHubUseCase(private val repository: IGitHubRepository) : IGitHubUseCase { override suspend fun user(name: String): UserPresentModel { val response = repository.user(name) return if (response != null) { UserPresentModel(response.id, response.reposUrl) } else { UserPresentModel.NONE } } }
今年
class GitHubUseCase(private val repository: IGitHubRepository) : IGitHubUseCase { override suspend fun user(name: String): Flow<UserPresentModel> = flow { val response = repository.user(name) emit( if (response != null) { UserPresentModel(response.id, response.reposUrl) } else { UserPresentModel.NONE } ) } override suspend fun feed(): Flow<FeedPresentModel> = flow { val response = repository.feed() emit( if (response != null) { FeedPresentModel(response.timelineUrl) } else { FeedPresentModel.NONE } ) } }
このサンプルではUseCase層を作っています。もしなければRepositoryのほうをFlowで返すようにすると思います。suspend functionよりFlowのほうが高機能なっていて、onStartやRxのようメソッドも生えているので合成などもしやすくなったかなと思います。
コールバックしかサポートしていない(Firebaseなど)ライブラリもsuspendCoroutineでコルーチン化していたやつもFlowでコルーチン化するようになりそうですね。
例:FirebaseのこういうやつもFlow化するだろう
override suspend fun getIdToken(): String = suspendCoroutine<String> { con -> FirebaseAuth.getInstance().currentUser.getIdToken(true) .addOnCompleteListener { task -> if (task.isSuccessful) { task.result?.token?.let { idToken -> con.resume(idToken) return@addOnCompleteListener } } con.resumeWithException(task.exception) } }
Coroutine、Flowの詳細な説明はとてもわかりやすい資料がありますので、僕は説明しません。いつも神資料に感謝。
ViewModelでコルーチンを起動する際にwithContextからliveData{}, viewModelScope.launchで起動し、あえてバッググランドのスレッドをどこにするか指定しなくなったこと
去年
class MainViewModel(private val useCase: IGitHubUseCase) : ViewModel() { val id = ObservableField<String>() val repoUrl = ObservableField<String>() val loading = ObservableField<Boolean>() val nameEdit = ObservableField<String>() fun fetch() = viewModelScope.launch(Dispatchers.Main) { loading.set(true) runCatching { withContext(Dispatchers.IO) { useCase.user(nameEdit.get() ?: "") } }.onSuccess { id.set(it.id) repoUrl.set(it.reposUrl) loading.set(false) }.onFailure { it.printStackTrace() loading.set(false) } } }
今年
class MainViewModel(private val useCase: IGitHubUseCase) : ViewModel() { val nameEdit = MutableLiveData<String>() val user: MutableLiveData<LoadState<UserPresentModel>> = MutableLiveData() val feed: LiveData<LoadState<FeedPresentModel>> = liveData { emitSource(useCase.feed().toLoadingState().asLiveData()) } fun fetch() { viewModelScope.launch(Dispatchers.Main) { useCase.user(nameEdit.value ?: "").toLoadingState().collect { user.value = it } } } val feedUrl = feed.map { when (it) { is LoadState.Loaded -> { it.value.timelineUrl } else -> "" } } }
去年はObservableFieldでもLiveDataも同じことですが、 viewModelScope.launch(Dispatchers.Main)で起動して、withContext(Dispatchers.IO) でスレッドを切り替えて、api実行し、返ってきた値をUIスレッドで変更していました。
今年は、JetPackのほうでコルーチンで簡単に扱えるようになったのも大きいですね。liveDataで起動し、flowの結果をLiveDataに変換して突っ込むだけ。 UIのボタンなどをトリガーにするものもviewModelScope.launch(Dispatchers.Main) で起動するけど、flowの処理のスレッドやストリームの処理のスレッドを意識することなくいい感じにやってくれています。
今回の話題に関係ないけど、なにげにLiveDataにmapが追加されたのも嬉しい。Transformations.map()でくくるのめんどくさかったですよね。
あとがき
なんかflow素敵やん。Rxっぽい感じにまたなりそうな予感ですね。