Recast turns Kotlin Multiplatform coroutines into consumable iOS methods.
1. Define a suspending function in your Kotlin multiplatform project and annotate:
@Recast
suspend fun getUser(id: Int): User { ... }
2. At compile-time, an equivalent asynchronous [1] method is generated:
fun getUser(id: Int, callback: (Result<User>) -> Unit): Job
3. Which can be consumed in your iOS project:
getUser("12") { (result: Result<User>) -> Void in
let user: User? = result.getOrNull()
...
}
[1] Asynchronous: run on a background thread. See Limitations for more information.
When using Kotlin multiplatform to share code between Android and iOS, suspending functions cannot be directly called from an iOS project.
Recast automatically creates synchronous or asynchronous methods that can be consumed within your iOS project.
Recast uses annotation processing to generate Kotlin code that can be consumed within your iOS project.
The sample below shows how Recast can be integrated into a Multiplatform project:
- The iOS source set is updated to include the generated code from the annotation processor.
- Tasks that build iOS artifacts are updated to ensure the annotation processor is run beforehand.
repositories {
maven { url "https://dl.bintray.com/andrewemery/recast" }
}
kotlin {
jvmMain {
kotlin.srcDirs += "build/generated/source/kaptKotlin/main"
dependencies {
implementation "com.andrewemery.recast:recast-jvm:0.5"
}
}
iosArm64Main {
kotlin.srcDirs += "build/generated/source/kaptKotlin/main"
dependencies {
implementation "com.andrewemery.recast:recast-iosArm64:0.5"
}
}
iosX64Main {
kotlin.srcDirs += "build/generated/source/kaptKotlin/main"
dependencies {
implementation "com.andrewemery.recast:recast-iosX64:0.5"
}
}
}
afterEvaluate {
tasks.each { task ->
if (task.name.startsWith('link') && task.name.contains('Ios')) {
task.dependsOn 'kaptKotlinJvm'
}
}
}
dependencies {
kapt "com.andrewemery.recast:recast-compiler:0.5"
}
When the @Recast
annotation is applied:
@Recast
suspend fun getUser(id: Int): User { ... }
At compile-time, an equivalent asynchronous method is generated:
fun getUser(id: Int, callback: (Result<User>) -> Unit): Job
Which can then be consumed in your iOS project:
getUser("12") { (result: Result<User>) -> Void in
let user: User? = result.getOrNull()
...
}
By default, the generated asynchronous method does not take a coroutine scope as a parameter.
In such instances, the GlobalScope
is used to scope the coroutine.
If a custom scope is desired, the annotation can be adjusted to suit:
@Recast(scoped = true)
suspend fun getUser(id: Int): User { ... }
Which generates:
fun getUser(id: Int, scope: CoroutineScope, callback: (Result<User>) -> Unit): Job
Which can then be consumed in your iOS project:
let scope = CoroutinesKt.supervisorScope()
getUser("12", scope) { (result: Result<User>) -> Void in
let user: User? = result.getOrNull()
...
}
As shown above, the generated method can pass a coroutine scope that can be used to cancel all scoped operations:
let scope = CoroutinesKt.supervisorScope()
getUser("12", scope) { ... }
scope.cancel()
The asynchronous method also returns a Job
that can be used to cancel a single operation:
let job = getUser("12") { ... }
job.cancel()
If desired, a suffix can be added to the method name:
@Recast(suffix = "Async")
suspend fun getUser(id: Int): User { ... }
To produce:
fun getUserAsync(id: Int, callback: (Result<User>) -> Unit): Job
At present, multithreaded coroutines are currently unsupported in Kotlin/Native (the platform that iOS applications target).
To workaround this, coroutines annotated with @Recast
must be called from the iOS main thread.
The result of the asynchronous operation will also be returned to the main thread:
getUser("12") { (result: Result<User>) -> Void in // must be called on the main thread
let user: User? = result.getOrNull() // result returned to the main thread
...
}
An alternative to this approach is to use the RecastSync
annotation and manage threading within your iOS project natively.
When the RecastSync
annotation is applied:
@RecastSync
suspend fun getUser(id: Int): User { ... }
At compile-time, an equivalent synchronous method is generated (with Sync
added as a suffix to the method name):
fun getUserSync(id: Int): User
Which can then be consumed in your iOS project:
let user: User = getUserSync("12")
If desired, a custom method suffix can be used instead:
@RecastSync(suffix = "Synchronous")
suspend fun getUser(id: Int): User { ... }
To produce:
fun getUserSynchronous(id: Int): User
Recast annotations can be added to the following objects:
- Function
- Class
- Interface
- Object
When the annotation is added to a class, interface or object:
@Recast
class UserRepository {
suspend fun getUser(id: Int): User { ... }
suspend fun getUsers(): List<User> { ... }
}
Equivalent extension functions are generated for all suspending functions on the target class:
fun UserRepository.getUser(id: Int, callback: (Result<User>) -> Unit): Job = ...
fun UserRepository.getUsers(callback: (Result<List<User>>) -> Unit): Job = ...
Annotations set against methods override those set against a parent. For example, the following code:
@Recast(scoped = true)
class UserRepository {
@Recast(suffix = "Async")
suspend fun getUser(id: Int): User { ... }
}
Generates the following method (note how the the method annotation has overriden the parent):
fun UserRepository.getUserAsync(id: Int, callback: (Result<User>) -> Unit): Job = ...
- Integrate with Kotlin/Native annotation processor.