こんにちは。いつ と申します。
普段はMiRm Development Teamの一員として、MiRmというゲームサーバーホスティングサービスの開発をしております。
今回はKotlin Coroutinesを用いた非同期処理のコードについて、より効果的な書き方を提案したいと思います。
どういう処理をするのか
例として「Android上において、RestfulなWebAPIから取得した情報の内容によってFragmentに表示する内容を変える」というシチュエーションを考えていくことにします。
非メインスレッドから描画処理を行うと当然ながらエラーが発生しますし、逆にメインスレッドでネットワークアクセスをしてもエラーが発生します。特にAndroid上での非同期処理の実装は悩みどころです。
使用するクラスの説明
- WebAPI WebAPIにアクセスし、getやpostを行うクラス。
- ResponseModel WebAPIから取得したレスポンスのオブジェクト。
各クラスの詳細な処理は割愛します。
ResponseModel.kt
data class ResponseModel(
val statusCode: Int,
val data1: String
) {
companion object {
const val STATUS_SUCCESS = 0
const val STATUS_ERROR = 1
const val STATUS_DATABASE_ERROR = 2
const val STATUS_NOT_FOUND = 3
}
}
WebAPI.kt
object WebAPI {
fun getUserData(url: String): ResponseModel {
// get処理
val model = ResponseModel(ResponseMode.STATUS_SUCCESS, url)
return model
}
}
以上二つのクラスをベースに考えていきます。
何も考えずに書くと...
以下、何も考えずに非同期処理を実行する処理を書いてみます。ボタンを押すとonButtonClick()が呼ばれるとします。
__2020/5/19 修正__ @sdkei さんにご指摘いただきました。ありがとうございます。
GlobalScope.async(Dispatchers.Default) {
WebAPI.getUserData("...")
}.await().let{ /* 処理 */ }
// から
withContext(Dispatchers.IO) {
WebAPI.getUserData("...")
}.let { /* 処理 */ }
/*
に修正しました。
・async/await -> withContext
・Dispatchers.Default -> Dispatchers.IO
*/
【なぜ修正したのか】
-
GlobalScope.async(...) {}.await().let{}
だとcoroutineを新たに生成するため、リソースを浪費してしまうから。1 - Dispatchers.DefaultはCPUのコア数ぶんしかないため、スレッドが足りなくなるから。2
以下本文
fun onButtonClick() = GlobalScope.launch(Dispatchers.Main) {
showToast("Loading...")
withContext(Dispatchers.IO) {
WebAPI.getUserData("https://...")
}.let {
when (it.statusCode) {
ResponseModel.STATUS_SUCCESS -> {
setText("Success!!")
showDialog()
}
ResponseModel.STATUS_ERROR -> setText("Error!!")
ResponseModel.STATUS_DATABASE_ERROR -> setText("Database Error!!")
ResponseModel.STATUS_NOT_FOUND -> setText("Not Found!!")
}
}
showToast("Process Finished!!")
}
どういった問題が起こるか
- インデントが多く、ブロックが何重にもネストしていて非常に見にくい
- statusCodeの種類が増えると、その分条件分岐が増えて拡張が困難
- WebAPIごとに共通な処理が多いため、コードが冗長になる
- ビジネスロジックを不用意に荒らすことになりかねない
などといった問題が発生し、バージョンアップ時の拡張性が損なわれたり、バグが見つかりにくくなるなどコードの保守にも影響する可能性があります。
提案する書き方
上記の問題を解消するために、関数オブジェクトとinvokeを用いたコードの書き方を提案します。
提案する書き方だと、クラスの量は少し増えますがコードの共通化も図れ、非常に見やすいものとなります。
BaseManager
ベースとなる非同期マネージャクラスです。エラー処理などを共通化します。
package dev.itsu.test
open class BaseManager<T: BaseManager<T>> {
protected var onInitialize: () -> Unit = {}
protected var onFinish: () -> Unit = {}
protected var onError: () -> Unit = {}
// 処理開始前に呼ばれる関数
fun onInitialize(func: () -> Unit): T {
this.onInitialize = func
return this as T
}
// 処理終了時に呼ばれる関数
fun onFinish(func: () -> Unit): T {
this.onFinish = func
return this as T
}
// エラー時に呼ばれる関数
fun onError(func: () -> Unit): T {
this.onError = func
return this as T
}
}
GetUserDataManager
実際の非同期処理の実装クラスです。
package dev.itsu.test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
class GetUserDataManager: BaseManager<GetUserDataManager>() {
private var onSuccess: (model: ResponseManager) -> Unit = {}
private var onDatabaseError: () -> Unit = {}
private var onNotFound: () -> Unit = {}
fun getUserData(url: String) = GlobalScope.launch(Dispatchers.Main) {
onInitialize.invoke()
withContext(Dispatchers.IO) {
WebAPI.getUserData(url)
}.let {
when (it.statusCode) {
ResponseModel.STATUS_SUCCESS -> onSuccess(it)
ResponseModel.STATUS_ERROR -> onError()
ResponseModel.STATUS_DATABASE_ERROR -> onDatabaseError()
ResponseModel.STATUS_NOT_FOUND -> onNotFound()
}
onFinish.invoke()
}
}
fun onSuccess(func: (model: ResponseModel) -> Unit): GetUserDataManager {
this.onSuccess = func
return this
}
fun onDatabaseError(func: () -> Unit): GetUserDataManager {
this.onDatabaseError = func
return this
}
fun onNotFound(func: () -> Unit): GetUserDataManager {
this.onNotFound = func
return this
}
}
Presenter(非同期処理実行クラス)
実際にこのクラスで非同期処理を実行します。
fun onButtonClick() {
GetUserDataManager()
.onInitialize { showToast("Loading...") }
.onFinish { showToast("Process Finished!!") }
.onError { setText("Error!") }
.onDatabaseError { setText ("Database Error") }
.onNotFound { setText ("Not Found.") }
.onSuccess {
setText("Your name is ${it.data1}")
showDialog()
}
.getUserData("https://...")
}
結果
上記のPresenterクラスのように、条件分岐は無しに、ネストも最小限になって非常にコードが見やすくなりました。また、BaseManagerクラスにてonErrorなどのWebAPIにおける共通の処理を書くことによって、GetUserDataManagerをはじめとするWebAPIのマネージャクラスにおける実装も最小限のものとなりました。
いかがだったでしょうか。
このように実装次第で、ビジネスロジック部分の処理をより簡潔に、見やすく書くことができるようになります。
自分もまだまだkotlin初心者ですが、実装を工夫して開発に臨んでいきたいと思います。
最後まで読んでいただき、ありがとうございました。
-
@sdkei さんによるコメント 及び Kotlin coroutine withContext とは ↩
-
@sdkei さんによるコメント 及び GitHub kotlinx.coroutines ↩