reakt-native-toolkit

NPM Maven Central

This toolkit allows you to combine React Native with Kotlin Multiplatform Mobile (KMM) by generating native modules for iOS and Android from Kotlin common code and supplying you with utilities to expose Kotlin Flows directly to React Native.

Installation

Prerequisite: Your project must be a Kotlin Multiplatform Mobile project

Add the KSP gradle plugin to your multiplatform project’s build.gradle.kts file, if you have subprojects, add it to the subject project’s build.gradle.kts file.

plugins {
    // from gradlePluginPortal()
    id("com.google.devtools.ksp") version "1.7.20-1.0.6"
}

Then add the reakt-native-toolkit to the commonMain source set dependencies.

implementation("de.voize:reakt-native-toolkit:<version>")

Then add reakt-native-toolkit-ksp to the KSP configurations:

dependencies {
    add("kspAndroid", "de.voize:reakt-native-toolkit-ksp:<version>")
    add("kspIosX64", "de.voize:reakt-native-toolkit-ksp:<version>")
    add("kspIosArm64", "de.voize:reakt-native-toolkit-ksp:<version>")
}

To use the JS utilities install them with:

yarn add reakt-native-toolkit

Usage

Native Module generation

To generate a native module, annotate a Kotlin class in the commonMain source set with @ReactNativeModule and add the @ReactNativeMethod annotation to the methods you want to expose to React Native.

import de.voize.reaktnativetoolkit.annotation.ReactNativeMethod
import de.voize.reaktnativetoolkit.annotation.ReactNativeModule

@ReactNativeModule("Calculator")
class CalculatorRNModule {
    @ReactNativeMethod
    fun add(a: Int, b: Int): Int {
        return a + b
    }
}

The toolkit will generate CalculatorRNModuleAndroid and CalculatorRNModuelIOS for you.

You can now add CalculatorRNModuleAndroid to your native modules package in androidMain:

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import com.example.calculator.CalculatorRNModuleAndroid
import kotlinx.coroutines.CoroutineScope

class MyRNPackage(coroutineScope: CoroutineScope) : ReactPackage {
    override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
        return emptyList()
    }

    override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
        return listOf<NativeModule>(
            CalculatorRNModuleAndroid(reactContext, coroutineScope),
            // ...
        )
    }
}

The CalculatorRNModuleIOS class will be compiled into your KMM projects shared framework and can be consumed in your iOS project in the extraModules of your RCTBridgeDelegate:

import shared // your KMM project's shared framework

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
  // ...

    func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
        return [CalculatorRNModuleIOS(...)]
    }
}

To do dependency injection or to supply the coroutineScope property you can wrap your RNModuleIOS classes in Kotlin in the iosMain source set and call constructors there:

import com.example.calculator.CalculatorRNModuleIOS
import kotlinx.coroutines.CoroutineScope
import react_native.RCTBridgeModuleProtocol

class MyIOSRNModules {
    val coroutineScope = CoroutineScope(Dispatchers.Default)

    fun createNativeModules(): List<RCTBridgeModuleProtocol> {
        return listOf(
            CalculatorRNModuleIOS(coroutineScope),
            // ...
        )
    }
}

And use this wrapper in Swift:

import shared // your KMM project's shared framework

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
    // ...

    func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
        return MyIOSRNModules().createNativeModules()
    }
}

Expose Kotlin flows to React Native

The toolkit allows you to directly expose Kotlin Flows to React Native and supplies you with utilities to interact with them.

A flow exposed to React Native could look like this:

import de.voize.reaktnativetoolkit.annotation.ReactNativeMethod
import de.voize.reaktnativetoolkit.annotation.ReactNativeModule
import de.voize.reaktnativetoolkit.util.toReact

@ReactNativeModule("Counter")
class CounterRNModule {
    private val counter = MutableStateFlow(0)

    @ReactNativeMethod
    suspend fun count(previous: String?) = counter.toReact(previous)

    @ReactNativeMethod
    suspend fun increment() {
        counter.value = counter.value + 1
    }
}

Notice how the count method that is marked as a @ReactNativeMethod uses the extension function Flow<T>.toReact(previous). toReact will JSON serialize the value of the flow and suspend until the value is different from the previous (see in source). This is why previous is a string.

On the JS side we interact with this suspended value using the useFlow hook:

import { useFlow, Next } from "reakt-native-toolkit";
import { NativeModules } from "react-native";

interface CounterInterface {
  count: Next<string>;
  increment: () => Promise<void>;
}

const Counter = NativeModules.Counter as CounterInterface;

function useCounter() {
  const count = useFlow(Counter.count);

  return {
    count,
    increment: Counter.increment,
  };
}

With useFlow(Counter.count) we can “subscribe” to the flow value. The hook will trigger a rerender whenever the flow value changes. Remember that toReact suspends until the value changes, so what useFlow does is to wait for this suspension, emit the value as soon as it is available and immediately start waiting again for the next value. useFlow therefore always establishes a new open promise when the previous one resolved.

The count method of the Counter native module is typed as Next<T> which is a type alias for (currentValue: string | null) => Promise<string>. Although internally Next<T> is only operating on string the useFlow hook type is able to restore the type T in Next<T>.

GitHub

View Github