Callbacks ((...) => T)
Callbacks are functions created in one language and passed to another to provide a way to "call back" later.
Nitro has a clever reference counting system to allow users to use callbacks/functions from JS safely, and without any limitations. Each callback holds a strong reference on the native side and can be called as often as needed. Once the callback is no longer used, it will be safely deleted from memory.
- TypeScript
- Swift
- Kotlin
- C++
import { type HybridObject, iOS } from 'nitro-core'
@iOS(...)
export declare class Server extends HybridObject {
start(onNewUserJoined: (user: User) => void): void
}
func start(onNewUserJoined: any Callback<User, Void>) {
onNewUserJoined(user)
}
fun start(onNewUserJoined: Callback<User, Void>) {
onNewUserJoined(user)
}
void start(const Callback<void(const User&)>& onNewUserJoined) {
onNewUserJoined(user);
}
Events
Since callbacks can be safely kept in memory for longer and called multiple times, Nitro does not have a special type for an "event". It is simply a function you store in memory and call later, just like in a normal JS class. ✨
- TypeScript
- Swift
- Kotlin
- C++
type Orientation = "portrait" | "landscape"
@iOS(...)
export declare class DeviceInfo extends HybridObject {
listenToOrientation(onChanged: (o: Orientation) => void): void
}
export * from '#nitro/DeviceInfo'
const deviceInfo = // ...
deviceInfo.listenToOrientation((o) => {
console.log(`Orientation changed to ${o}!`)
})
func listenToOrientation(onChanged: any Callback<Orientation, Void>) {
self.listeners.append(onChanged)
}
func onRotate() {
for listener in self.listeners {
listener(newOrientation)
}
}
fun listenToOrientation(onChanged: Callback<Orientation, Unit>) {
this.listeners.add(onChanged)
}
fun onRotate() {
for (listener in this.listeners) {
listener(newOrientation)
}
}
void listenToOrientation(const Callback<void(Orientation)>& onChanged) {
this->listeners.push_back(onChanged);
}
void onRotate() {
for (const auto& listener: this->listeners) {
listener(newOrientation);
}
}
Callbacks that return a value ((...) => T)
A Callback can also return a value:
@iOS(...)
export declare class Math extends HybridObject {
calculate(getValue: () => number): void
}
export * from '#nitro/Math'
Since JS callbacks could theoretically be called from any native Thread, callbacks are called asynchronously by default:
class HybridMath: HybridMathSpec {
func calculate(getValue: any Callback<(), Double>) {
Task {
let result = try await getValue() // async Double
}
}
}
Async by default, sync when needed
Callback has three calling conventions:
func call(callback: any Callback<(), Void>) {
callback.callAsync() // Promise<Void>
callback.callSync() // Void (blocking)
callback.callAndForget() // Void
}
Additionally, the call operator overload either chooses callAndForget() when the callback returns Void, or callAsync() when it returns a value.
func callVoid(callback: any Callback<(), Void>) {
callback() // Void (just like callAndForget())
}
func callResult(callback: any Callback<(), Double>) {
callback() // Promise<Double> (just like callAsync())
}
Use callSync() only when you can ensure that you call it on the JS Runtime's Thread - otherwise it will throw.
Inline Callbacks
If your callback never escapes the calling function, use Inline<...> as a performance optimization:
@iOS(...)
export declare class Math extends HybridObject {
calculate(getValue: Inline<() => number>): void
}
export * from '#nitro/Math'
InlineCallback<...> allows you to call back to JS inline, or escape to a Callback<...> just like before:
class HybridMath: HybridMathSpec {
func calculate(getValue: borrowing some InlineCallback<(), Double> & ~Copyable & ~Escapable) {
let result = getValue() // Double
let callbackForLater = getValue.escape() // any Callback<(), Double>
}
}
How was it before Nitro?
Conventionally (in legacy React Native Native Modules), a native method could only have a maximum of two callbacks, one "success" and one "failure" callback. Once one of these callbacks is called, both will be destroyed and can no longer be called later. This is why React Native introduced "Events" as a way to call into JS more than just once. This also meant that an asynchronous function could not have any callbacks, since a Promise's resolve and reject functions are already two callbacks. For example, this was not possible:
interface Camera {
startRecording(onStatusUpdate: () => void,
onRecordingFailed: () => void,
onRecordingFinished: () => void): Promise<void>
}
Thanks to Nitro's clever reference system, functions can be safely held in memory and called as many times as you like, just like in a normal JS class. This makes "Events" obsolete, and allows using as many callbacks per native method as required.