Skip to content

Recasts Kotlin multiplatform coroutines into consumable iOS methods

Notifications You must be signed in to change notification settings

andrewemery/recast

Repository files navigation

Recast

Recast turns Kotlin Multiplatform coroutines into consumable iOS methods.

Introduction

 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.

Overview

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.

Integration

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:

  1. The iOS source set is updated to include the generated code from the annotation processor.
  2. 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"
}

Recast: asynchronous

Basics

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()
    ...
}

Coroutine scope

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()
    ...
}

Cancellation

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()

Method suffix

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

Asynchronous limitations

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.

Recast: synchronous

Basics

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")

Method suffix

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

Annotation support

Basics

Recast annotations can be added to the following objects:

  1. Function
  2. Class
  3. Interface
  4. 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 = ...

Overrides

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 = ...

Roadmap

  1. Integrate with Kotlin/Native annotation processor.

About

Recasts Kotlin multiplatform coroutines into consumable iOS methods

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages