Coming from Swift or Kotlin
Swift and Kotlin concepts map to Lucent one to one. A module written in Swift, Kotlin and a JS binding becomes one Lucent module.
| Swift | Kotlin | Lucent |
|---|---|---|
struct | data class | an object type, type Point = { x: number; y: number }: copied at the boundary |
class | class | class: reference-counted, as in Swift |
protocol | interface | interface, used with implements |
| a delegate | a listener | a class that implements the SDK's protocol or interface |
async | suspend | async: runs on the Lucent thread |
@MainActor | Dispatchers.Main | main(() => …) from lucent:thread |
T? | T? | T | null |
throws | exceptions | throw error(code, message); SDK errors arrive as errors with a code |
if #available(iOS 17, *) | Build.VERSION.SDK_INT >= 34 | available("ios", 17), available("android", 34) |
enum | enum class | enum |
SDK names stay as you know them: Swift's names on iOS (UIDevice.current), Java's on Android (Build.MODEL). Nested types join with _, as in UIImpactFeedbackGenerator_FeedbackStyle and Build_VERSION.
One module instead of three files
Section titled “One module instead of three files”An Expo module of the clipboard needs Swift, Kotlin and a JavaScript binding. This is a short version of each, in the Expo Modules API:
import ExpoModulesCore
public class ClipboardModule: Module { public func definition() -> ModuleDefinition { Name("Clipboard")
AsyncFunction("getStringAsync") { () -> String in UIPasteboard.general.string ?? "" }.runOnQueue(.main)
AsyncFunction("setStringAsync") { (text: String) in UIPasteboard.general.string = text }.runOnQueue(.main) }}class ClipboardModule : Module() { private val clipboard get() = appContext.reactContext!!.getSystemService(ClipboardManager::class.java)
override fun definition() = ModuleDefinition { Name("Clipboard")
AsyncFunction("getStringAsync") { clipboard.primaryClip?.getItemAt(0)?.text?.toString() ?: "" }.runOnQueue(Queues.MAIN)
AsyncFunction("setStringAsync") { text: String -> clipboard.setPrimaryClip(ClipData.newPlainText(null, text)) }.runOnQueue(Queues.MAIN) }}import { requireNativeModule } from "expo-modules-core";
const Clipboard = requireNativeModule("Clipboard");
export const getStringAsync = (): Promise<string> => Clipboard.getStringAsync();export const setStringAsync = (text: string): Promise<void> => Clipboard.setStringAsync(text);The Lucent module does the same in one file, and JavaScript imports it directly:
import { PLATFORM } from "lucent:platform";import { UIPasteboard } from "lucent:ios/UIKit";import { ClipboardManager, ClipData } from "lucent:android/android.content";import { appContext } from "lucent:android";import { main } from "lucent:thread";
export async function getStringAsync(): Promise<string> { if (PLATFORM === "ios") { return main(() => UIPasteboard.general.string ?? ""); } else { return main(() => { const clip = appContext().getSystemService(ClipboardManager)?.getPrimaryClip(); return clip?.getItemAt(0)?.getText() ?? ""; }); }}
export async function setStringAsync(text: string): Promise<void> { if (PLATFORM === "ios") { await main(() => { UIPasteboard.general.string = text; }); } else { await main(() => { const clip = ClipData.newPlainText(null, text); if (clip) appContext().getSystemService(ClipboardManager)?.setPrimaryClip(clip); }); }}See the C++
// Generated by Lucent from clipboard.lucent.ts. Do not edit.#import <UIKit/UIKit.h>#include <lucent/platform/ios.h>#include "m_clipboard.h"
#pragma push_macro("getStringAsync")#undef getStringAsync#pragma push_macro("setStringAsync")#undef setStringAsync#pragma push_macro("text")#undef text
namespace lucent_app {
lucent::Promise<lucent::String> m_clipboard::getStringAsync() {#line 8 "clipboard.lucent.ts" {#line 9 "clipboard.lucent.ts" lucent::Fn<lucent::String()> v0_ = lucent::Fn<lucent::String()>([]() mutable -> lucent::String {#line 9 "clipboard.lucent.ts" lucent::NativeRef v0_ = lucent::objc::wrap([UIPasteboard generalPasteboard], "UIPasteboard.general"); lucent::Opt<lucent::String> v1_ = lucent::objc::fromNSStringOpt([((UIPasteboard*)lucent::objc::unwrap(v0_)) string]); bool v3_ = !v1_.has(); lucent::String v6_{}; if (v3_) { v6_ = lucent::String(); } else { v6_ = v1_.value(); }#line 9 "clipboard.lucent.ts" return v6_; });#line 9 "clipboard.lucent.ts" lucent::Promise<lucent::String> v1_ = lucent::runOnMain(v0_); lucent::String v2_ = co_await v1_; co_return v2_; }}
lucent::Promise<void> m_clipboard::setStringAsync(lucent::String p0_) {#line 19 "clipboard.lucent.ts" {#line 20 "clipboard.lucent.ts" lucent::Fn<void()> v1_ = lucent::Fn<void()>([text = p0_]() mutable -> void {#line 21 "clipboard.lucent.ts" lucent::NativeRef v0_ = lucent::objc::wrap([UIPasteboard generalPasteboard], "UIPasteboard.general"); lucent::Opt<lucent::String> v2_ = lucent::Opt<lucent::String>(text); (void)({ auto v_ = v2_; [((UIPasteboard*)lucent::objc::unwrap(v0_)) setString:lucent::objc::ifPresent(v_, [&](const auto& x_) { return lucent::objc::toNSString(x_); })]; v_; }); });#line 20 "clipboard.lucent.ts" lucent::Promise<void> v2_ = lucent::runOnMain(v1_); co_await v2_; } co_return;}
void m_clipboard::init() {}
} // namespace lucent_app
#pragma pop_macro("text")#pragma pop_macro("setStringAsync")#pragma pop_macro("getStringAsync")// Generated by Lucent from clipboard.lucent.ts. Do not edit.#include <lucent/platform/android.h>#include "m_clipboard.h"
#pragma push_macro("clip")#undef clip#pragma push_macro("getStringAsync")#undef getStringAsync#pragma push_macro("setStringAsync")#undef setStringAsync#pragma push_macro("text")#undef text
namespace lucent_app {
lucent::Promise<lucent::String> m_clipboard::getStringAsync() {#line 8 "clipboard.lucent.ts" {#line 11 "clipboard.lucent.ts" lucent::Fn<lucent::String()> v0_ = lucent::Fn<lucent::String()>([]() mutable -> lucent::String {#line 12 "clipboard.lucent.ts" lucent::NativeRef v0_ = lucent::jni::appContext(); lucent::Opt<lucent::NativeRef> v1_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v0_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/Context"); static auto id_ = lucent::jni::method(cls_, "getSystemService", "(Ljava/lang/Class;)Ljava/lang/Object;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_, lucent::jni::findClass("android/content/ClipboardManager")); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); bool v3_ = !v1_.has(); lucent::Opt<lucent::NativeRef> v8_{}; if (v3_) { v8_ = lucent::Opt<lucent::NativeRef>(lucent::undefined); } else { lucent::NativeRef v4_ = v1_.value(); v8_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v4_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipboardManager"); static auto id_ = lucent::jni::method(cls_, "getPrimaryClip", "()Landroid/content/ClipData;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); }#line 12 "clipboard.lucent.ts" lucent::Opt<lucent::NativeRef> clip = v8_;#line 13 "clipboard.lucent.ts" bool v11_ = !clip.has(); lucent::Opt<lucent::String> v24_{}; if (v11_) { v24_ = lucent::Opt<lucent::String>(lucent::undefined); } else { lucent::NativeRef v12_ = clip.value(); lucent::Opt<lucent::NativeRef> v14_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v12_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipData"); static auto id_ = lucent::jni::method(cls_, "getItemAt", "(I)Landroid/content/ClipData$Item;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_, static_cast<jint>(lucent::toInt32(0.0))); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); bool v16_ = !v14_.has(); lucent::Opt<lucent::String> v21_{}; if (v16_) { v21_ = lucent::Opt<lucent::String>(lucent::undefined); } else { lucent::NativeRef v17_ = v14_.value(); v21_ = [&]() -> lucent::Opt<lucent::String> { JNIEnv* env = lucent::jni::env(); auto recv_ = v17_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipData$Item"); static auto id_ = lucent::jni::method(cls_, "getText", "()Ljava/lang/CharSequence;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_); lucent::jni::check(env); return lucent::jni::charSequenceToStringOpt(env, r_); }(); }#line 13 "clipboard.lucent.ts" v24_ = v21_; }#line 13 "clipboard.lucent.ts" bool v26_ = !v24_.has(); lucent::String v29_{}; if (v26_) { v29_ = lucent::String(); } else { v29_ = v24_.value(); }#line 13 "clipboard.lucent.ts" return v29_; });#line 11 "clipboard.lucent.ts" lucent::Promise<lucent::String> v1_ = lucent::runOnMain(v0_); lucent::String v2_ = co_await v1_; co_return v2_; }}
lucent::Promise<void> m_clipboard::setStringAsync(lucent::String p0_) {#line 19 "clipboard.lucent.ts" {#line 24 "clipboard.lucent.ts" lucent::Fn<void()> v1_ = lucent::Fn<void()>([text = p0_]() mutable -> void {#line 25 "clipboard.lucent.ts" lucent::Opt<lucent::NativeRef> clip = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipData"); static auto id_ = lucent::jni::staticMethod(cls_, "newPlainText", "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;"); auto r_ = env->CallStaticObjectMethod(cls_, id_, ({ auto s_ = lucent::Opt<lucent::String>(lucent::null); s_.has() ? lucent::jni::toJString(env, s_.get()) : nullptr; }), ({ auto s_ = lucent::Opt<lucent::String>(text); s_.has() ? lucent::jni::toJString(env, s_.get()) : nullptr; })); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }();#line 26 "clipboard.lucent.ts" bool v4_ = lucent::truthy(clip); if (v4_) { lucent::NativeRef v5_ = lucent::jni::appContext(); lucent::Opt<lucent::NativeRef> v6_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v5_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/Context"); static auto id_ = lucent::jni::method(cls_, "getSystemService", "(Ljava/lang/Class;)Ljava/lang/Object;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_, lucent::jni::findClass("android/content/ClipboardManager")); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); bool v8_ = !v6_.has(); if (v8_) { } else { lucent::NativeRef v9_ = v6_.value(); lucent::NativeRef v11_ = clip.value(); [&]() -> lucent::Undefined { JNIEnv* env = lucent::jni::env(); auto recv_ = v9_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipboardManager"); static auto id_ = lucent::jni::method(cls_, "setPrimaryClip", "(Landroid/content/ClipData;)V"); env->CallVoidMethod(lucent::jni::unwrap(recv_), id_, lucent::jni::unwrap(v11_)); lucent::jni::check(env); return lucent::undefined; }(); } } });#line 24 "clipboard.lucent.ts" lucent::Promise<void> v2_ = lucent::runOnMain(v1_); co_await v2_; } co_return;}
void m_clipboard::init() {}
} // namespace lucent_app
#pragma pop_macro("text")#pragma pop_macro("setStringAsync")#pragma pop_macro("getStringAsync")#pragma pop_macro("clip")- The JS binding is gone: the module's exports are the API, typed from the source.
main()replacesrunOnQueue. APIs that Swift marks@MainActorcompile only inside it.- Kotlin's
!!and?.become Lucent's?.and??, since unannotated Java references are nullable.
The full port of expo-clipboard is in the examples.