Convert Rx to coroutine - libgdx example
TL;DR; For REST requests in Kotlin, I think coroutines are a better choice than Rx. The code feels more natural to the given environment then with Rx.
In our game we use a SplashScreen to
- Load all assets with progress bar
- Check if the game client is still compatible with the REST API and if so, perform an auth request to check if the user is logged in.
A player can only login, if the game is compatible with the REST API. (I see to solutions to this problem: Either check globally if the client is compatible with the API or check every API endpoint if is compatible. The later would make sense if the API is huge and fragmented, but in my case it's very small, so I check it once)
The following Code uses RxJava to perform these tasks:
SplashScreen Class:Observable.zip(
performInitialRequests(),
assetLoaderSubject,
BiFunction { pair: Pair<ScreenId, UserProfile>, signal: Signal ->
pair
}
)
.subscribe { (stateId, profile) ->
//GAME READY
}
}
fun performInitialRequest() {
return App.maintenanceApi
.checkVersion(VersionManager.toRequest()) //returns Observable<HttpStatus>
.onErrorResumeNext { e: Throwable ->
showMessage(context.messageBundle.get("initialRequest.failed"))
Gdx.app.debug(LOG_TAG, "", e)
Observable.empty()
}
.flatMap {
if (it.statusCode == HttpStatus.SC_NO_CONTENT) {
performAuthenticationRequest() //Try to login
} else {
Observable.just(Pair(ScreenId.PLAY_STORE, UserProfile.EMPTY_PROFILE)) //User needs to update the game
}
}
}
}
override fun act(delta: Float) {
if (context.assetManager.update()) {
//... update progress bar
assetLoaderSubject.onNext(Signal)
} else {
//... update progress bar
}
}
App.maintenanceApi implementation
override fun checkVersion(requestData: VersionRequest): Observable<HttpStatus> {
val request = Net.HttpRequest(Net.HttpMethods.GET)
request.url = "$BACKEND_URL/maintenance/check-version/${requestData.versionCode}"
return fromHttpRequest(request)
.map { it.status }
.observeOn(GdxScheduler.get()) //Back to render thread
}
We'd like to convert this code to Kotlin coroutines (Version 1.0.1, Kotlin 1.3, with KTX).
SplashScreen Class:ktxAsync {
val (stateId, profile) = performInitialRequests()
assetLoadedSignal.receive() //Wait for asset completion
//GAME READY
}
private suspend fun performInitialRequests(): Pair<ScreenId, UserProfile> {
return try {
val responseCode = App.maintenanceApi.checkVersion(VersionManager.toRequest())
if (responseCode.statusCode == HttpStatus.SC_NO_CONTENT) {
performAuthenticationRequest().awaitFirst()
} else {
Pair(ScreenId.PLAY_STORE, UserProfile.EMPTY_PROFILE)
}
} catch (e: Exception) {
showMessage(context.messageBundle.get("initialRequest.failed"))
Gdx.app.debug(LOG_TAG, "", e)
Pair(ScreenId.PLAY_STORE, UserProfile.EMPTY_PROFILE)
}
}
override fun act(delta: Float) {
if (context.assetManager.update()) {
//... update progress bar
assetLoadedSignal.offer(Unit)
} else {
//... update progress bar
}
}
App.maintenanceApi implementation
override suspend fun checkVersion(requestData: VersionRequest): HttpStatus {
val request = Net.HttpRequest(Net.HttpMethods.GET)
request.url = "$BACKEND_URL/maintenance/check-version/${requestData.versionCode}"
return HttpStatus(KtxAsync.httpRequest(request).statusCode)
}
The coroutine code is in my opinion clearer:
- In the above code, error handling in Rx is a little bit awkward. We return an
Observable.empty()
meaning the rest of the chain is not executed. But what then? A failed Request has to be reported to the user (seeshowMessage
) and delegate the user to a different screen. In the above implementation the user just stays on the splash screen. Of course, this is a bug, which I encountered during this refactoring. The Rx code is harder to understand in this case - for me at least. - Waiting for assets loaded and the request is in the coroutine version more natural - at least in a language
that is imperative and not purely functional.
With coroutine we can just call this to methods and let Kotlin do its "magic tricks"
Observable.zip
does the same job, but we have to deal with an additional concept. Usually, when you call two methods (method1
andmethod2
) one after another, you would expect thatmethod2
is called aftermethod1
is complete. To achieve the same result with Rx we have to go back to a special operator, in this casezip
to get this behaviour.
Rx is a very good solution when you have to push events through a system, for example UI events (this part of our game still uses Rx)