Coroutines support and general asynchronous operations utilities for libGDX applications.
Coroutines-based APIs greatly simplify asynchronous operations and avoid so-called callback hell. Some libGDX APIs -
like the Timer
- were not written with functional expressions in mind, often using abstract classes instead of
functional interfaces. Additionally, they are often tedious to call from within Kotlin. This module aims to hide
asynchronous code complexity with coroutines, as well as to improve existing asynchronous APIs with idiomatic Kotlin
extensions.
Before using ktx-async
, make sure to include the Kotlin coroutines library in your Gradle script:
// Groovy DSL:
api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
// Kotlin DSL:
api(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = coroutinesVersion)
The coroutinesVersion
must match the coroutines version that the ktx-async
library was compiled against -
otherwise it might cause runtime or compilation errors.
Before Kotlin 1.3, coroutines were an experimental feature and required the following Gradle configuration:
kotlin {
experimental {
coroutines 'enable'
}
}
If you are using a recent version of Kotlin and ktx-async
, this declaration is no longer required.
If using any coroutines-related classes results in a java.lang.NoClassDefFoundError
exception, make sure that
the backend module correctly picks up transitive dependencies of the core module. In particular, this error can be
seen on iOS with RoboVM if kotlinx-coroutines-core
or ktx-async
are included as implementation
dependencies
in the core Gradle module instead of api
.
ktx-async
was tested on desktop, Android and iOS, and should work on every major platform. If you encounter
any issues with launching the coroutines, do not hesitate to create an issue.
Please refer to Kotlin coroutines documentation or tutorials if you are having trouble with this section.
KtxAsync
is the default scope you should use instead of the GlobalScope
in your KTX applications. By default, it
will execute tasks on the main rendering thread. However, if you want to perform some actions asynchronously on
different threads, you can use AsyncExecutorDispatcher
that uses libGDX cross-platform AsyncExecutor
to execute
the tasks.
Before using KtxAsync
scope, make sure to call KtxAsync.initiate()
on the main rendering thread. This is not
strictly required if immediate dispatcher is not used, but as a rule of thumb, you should invoke this method in create
of your ApplicationListener
.
KTX providers 2 main implementations of coroutine dispatchers:
RenderingThreadDispatcher
: executes tasks on the main rendering thread. Available viaDispatchers.KTX
. Default dispatcher used by theKtxAsync
scope internally.RenderingScope
factory method allows to define aCoroutineScope
using theRenderingThreadDispatcher
.
AsyncExecutorDispatcher
: wraps libGDXAsyncExecutor
to execute the tasks. Can be initiated in the following ways:newSingleThreadAsyncContext()
factory method: creates anAsyncExecutor
with a single thread.newAsyncContext(threads)
factory method: creates anAsyncExecutor
with the given amount of threads.AsyncExecutorDispatcher
constructor: allows to wrap an existingAsyncExecutor
. Make sure to set thethreads
property to the actual threads amount if you rely on this value.
Additionally, ktx-async
provides the following utility methods:
onRenderingThread
: suspends the coroutine to execute a task on the main rendering thread and return its result. Should be used if you dispatch a coroutine with a non-rendering thread dispatcher and need to execute some task on the main rendering thread. Syntax sugar for quick context switch.isOnRenderingThread
: checks if the coroutine is being executed on the main rendering thread.skipFrame
: attempts to suspend the coroutine for a singlerender
frame of the application. The method is guaranteed to skip at least one frame, but - depending on the thread it was invoked on - it might skip multiple frames. Do not rely on this method for precise frame measurements.httpRequest
: allows to perform an asynchronous HTTP request.RenderingScope
factory function is the KTX rendering-thread version of the officialMainScope
. It creates a scope to launch coroutines in the rendering thread and that has a supervisor job so the whole scope can be cancelled at once.
Other asynchronous operations utilities include:
schedule
andinterval
functions were added to simplify libGDXTimer
API. Normally it requires the user to extend an abstract class - these utility functions allow using idiomatic Kotlin lambdas syntax.HttpRequestResult
is a thread-safe wrapper ofHttpResponse
that reads and caches HTTP response content. These objects are returned by the coroutines-based HTTP requests API.
Initiating coroutines context upon application creation:
import com.badlogic.gdx.ApplicationAdapter
import ktx.async.KtxAsync
class MyApp : ApplicationAdapter() {
override fun create() {
KtxAsync.initiate()
}
}
Starting a simple coroutine on the main rendering thread:
import com.badlogic.gdx.ApplicationAdapter
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
fun myFirstCoroutine() {
KtxAsync.launch {
println("Hello from the rendering thread! ${Thread.currentThread()}")
}
}
Starting a coroutine with a non-blocking suspension lasting 2 seconds:
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
fun coroutineWithDelay() {
KtxAsync.launch {
println("Before delay...")
val start = System.currentTimeMillis()
delay(timeMillis = 2000L)
val end = System.currentTimeMillis()
println("After delay: ${end - start} milliseconds passed.")
}
}
Starting a coroutine that skips a single rendering frame by suspension and resumes on the next render call:
import com.badlogic.gdx.ApplicationAdapter
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
import ktx.async.skipFrame
class MyApp : ApplicationAdapter() {
var frame = 0
override fun create() {
KtxAsync.initiate()
KtxAsync.launch {
println("On frame: $frame.")
skipFrame()
println("On frame: $frame.")
}
}
override fun render() {
frame++
}
}
Using AsyncExecutor
to perform tasks outside the rendering thread:
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ktx.async.KtxAsync
import ktx.async.newSingleThreadAsyncContext
fun asyncTask() {
val executor = newSingleThreadAsyncContext()
KtxAsync.launch {
println("Before async: ${Thread.currentThread()}")
withContext(executor) {
println("During async: ${Thread.currentThread()}")
}
println("After async: ${Thread.currentThread()}")
}
}
Using AsyncExecutor
with multiple threads to perform sophisticated multithreaded mathematical operations:
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
import ktx.async.newAsyncContext
fun multithreading() {
val executor = newAsyncContext(threads = 4)
KtxAsync.launch {
val a = async(executor) { 20 }
val b = async(executor) { 22 }
println("The answer to everything: ${a.await() + b.await()}.")
}
}
Manually switching between coroutine contexts:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ktx.async.KTX
import ktx.async.KtxAsync
import ktx.async.newSingleThreadAsyncContext
fun backAndForth() {
val executor = newSingleThreadAsyncContext()
KtxAsync.launch {
println("On rendering thread: ${Thread.currentThread()}")
withContext(executor) {
println("On async thread: ${Thread.currentThread()}")
withContext(Dispatchers.KTX) {
println("On rendering thread: ${Thread.currentThread()}")
}
}
}
}
Using utility methods to check and switch the threads:
import kotlinx.coroutines.launch
import ktx.async.*
fun threadSwitch() {
val executor = newSingleThreadAsyncContext()
KtxAsync.launch(executor) {
println("Executor context. ${isOnRenderingThread()}")
onRenderingThread {
println("Main KTX context. ${isOnRenderingThread()}")
}
println("Executor context. ${isOnRenderingThread()}")
}
}
Initiating AsyncExecutorDispatchers
to perform asynchronous operations outside the rendering thread:
import com.badlogic.gdx.utils.async.AsyncExecutor
import ktx.async.AsyncExecutorDispatcher
import ktx.async.newAsyncContext
import ktx.async.newSingleThreadAsyncContext
// Context with a single thread:
val singleThreaded = newSingleThreadAsyncContext()
// Context with multiple threads:
val multiThreaded = newAsyncContext(threads = 4)
// Context with a custom thread name pattern:
val multiThreadedWithNamedThreads = newAsyncContext(threads = 4, threadName = "MyThread")
// Context with an existing executor:
val executor = AsyncExecutor(2)
val fromExistingExecutor = AsyncExecutorDispatcher(executor, threads = 2)
Starting a coroutine, which performs an HTTP request and resumes on the main rendering thread after receiving the response (requires internet connection to run):
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
import ktx.async.httpRequest
fun http() {
KtxAsync.launch {
val response = httpRequest(url = "https://example.com")
println("""Reading response on ${Thread.currentThread()}. Website content:
${response.contentAsString}""")
}
}
Performing an asynchronous HTTP request via AsyncExecutor
:
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
import ktx.async.httpRequest
import ktx.async.newSingleThreadAsyncContext
fun httpAsync() {
val executor = newSingleThreadAsyncContext()
KtxAsync.launch {
val response = async(executor) { httpRequest(url = "https://example.com") }
response.await()
}
}
Cancelling a coroutine:
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ktx.async.KtxAsync
fun withCancel() {
val job = KtxAsync.launch {
delay(timeMillis = 1000L)
println("Should not execute this.")
}
job.cancel()
}
Creating a coroutine scope to confine jobs' lives to a specific class:
import com.badlogic.gdx.Screen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import ktx.async.httpRequest
import ktx.async.RenderingScope
class MyScreen: Screen, CoroutineScope by RenderingScope() {
// Implement your application screen here.
override fun hide() {
// Cancels any running coroutines when leaving the screen:
cancel()
}
// Starts coroutine in this screen's scope:
private fun loadSomething() = launch {
val result = httpRequest(url = "https://example.com")
webResultLabel.text = result.contentAsString
}
}
Creating a custom cancellable scope with an AsyncExecutor
dispatcher:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import ktx.async.AsyncExecutorDispatcher
import ktx.async.newSingleThreadAsyncContext
class MyScope(dispatcher: AsyncExecutorDispatcher)
: CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
val myScope = MyScope(newSingleThreadAsyncContext())
Scheduling a task executed on the main rendering thread after one second:
import ktx.async.schedule
val taskCallback = schedule(delaySeconds = 1f) {
println("Hello!")
}
Scheduling a task repeatedly executed on the main rendering thread after one second:
import ktx.async.interval
val taskCallback = interval(delaySeconds = 1f, intervalSeconds = 1f) {
println("Hello again!")
}
- Standard Kotlin coroutines libraries might be used along with custom thread pools. They do not offer the same level of compatibility with existing libGDX APIs though.