Skip to content

Commit

Permalink
Add Java Turbo Module Event Emitter example (facebook#44906)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#44906

Shows a proof of concept how '*strongly typed Turbo Module scoped*' `EventEmitters` can be used in a Java Turbo Module.

## Changelog:

[Android] [Added] - Add Java Turbo Module Event Emitter example

Reviewed By: javache

Differential Revision: D57530807
  • Loading branch information
christophpurrer authored and facebook-github-bot committed Jun 12, 2024
1 parent b6c3433 commit 7b6ffe6
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 9 deletions.
4 changes: 1 addition & 3 deletions packages/react-native-codegen/src/parsers/error-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,15 @@ function throwIfEventEmitterTypeIsUnsupported(
parser: Parser,
nullable: boolean,
untyped: boolean,
cxxOnly: boolean,
) {
if (nullable || untyped || !cxxOnly) {
if (nullable || untyped) {
throw new UnsupportedModuleEventEmitterPropertyParserError(
nativeModuleName,
propertyName,
propertyValueType,
parser.language(),
nullable,
untyped,
cxxOnly,
);
}
}
Expand Down
3 changes: 0 additions & 3 deletions packages/react-native-codegen/src/parsers/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,12 @@ class UnsupportedModuleEventEmitterPropertyParserError extends ParserError {
language: ParserType,
nullable: boolean,
untyped: boolean,
cxxOnly: boolean,
) {
let message = `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Further the EventEmitter property `;
if (nullable) {
message += `'${propertyValue}' must non nullable.`;
} else if (untyped) {
message += `'${propertyValue}' must have a concrete or void eventType.`;
} else if (cxxOnly) {
message += `'${propertyValue}' is only supported in C++ Turbo Modules.`;
}
super(nativeModuleName, propertyValue, message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ function buildEventEmitterSchema(
parser,
typeAnnotationNullable,
typeAnnotationUntyped,
cxxOnly,
);
const eventTypeResolutionStatus = resolveTypeAnnotationFN(
typeAnnotation.typeParameters.params[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.DeprecatedInNewArchitecture;
import com.facebook.react.common.annotations.StableReactNativeAPI;
Expand Down Expand Up @@ -54,6 +55,8 @@ public abstract class BaseJavaModule implements NativeModule {
public static final String METHOD_TYPE_PROMISE = "promise";
public static final String METHOD_TYPE_SYNC = "sync";

@Nullable protected CxxCallbackImpl mEventEmitterCallback;

private final @Nullable ReactApplicationContext mReactApplicationContext;

public BaseJavaModule() {
Expand Down Expand Up @@ -129,4 +132,9 @@ protected final ReactApplicationContext getReactApplicationContext() {
}
return null;
}

@DoNotStrip
private final void setEventEmitterCallback(CxxCallbackImpl eventEmitterCallback) {
mEventEmitterCallback = eventEmitterCallback;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,34 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
}
}

void JavaTurboModule::setEventEmitterCallback(
jni::alias_ref<jobject> jinstance) {
JNIEnv* env = jni::Environment::current();
auto instance = jinstance.get();
static jmethodID cachedMethodId = nullptr;
if (cachedMethodId == nullptr) {
jclass cls = env->GetObjectClass(instance);
cachedMethodId = env->GetMethodID(
cls,
"setEventEmitterCallback",
"(Lcom/facebook/react/bridge/CxxCallbackImpl;)V");
}

auto eventEmitterLookup =
[&](const std::string& eventName) -> AsyncEventEmitter<folly::dynamic>& {
return static_cast<AsyncEventEmitter<folly::dynamic>&>(
*eventEmitterMap_[eventName].get());
};

jvalue arg;
arg.l = JCxxCallbackImpl::newObjectCxxArgs([eventEmitterLookup = std::move(
eventEmitterLookup)](
folly::dynamic args) {
auto eventName = args.at(0).asString();
auto eventArgs = args.size() > 1 ? args.at(1) : nullptr;
eventEmitterLookup(eventName).emit(std::move(eventArgs));
}).release();
env->CallVoidMethod(instance, cachedMethodId, arg);
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class JSI_EXPORT JavaTurboModule : public TurboModule {
size_t argCount,
jmethodID& cachedMethodID);

void setEventEmitterCallback(jni::alias_ref<jobject> instance);

private:
// instance_ can be of type JTurboModule, or JNativeModule
jni::global_ref<jobject> instance_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ public NativeSampleTurboModuleSpec(ReactApplicationContext reactContext) {
super(reactContext);
}

protected final void emitOnPress() {
mEventEmitterCallback.invoke("onPress");
}

protected final void emitOnClick(String value) {
mEventEmitterCallback.invoke("onClick", value);
}

protected final void emitOnChange(ReadableMap value) {
mEventEmitterCallback.invoke("onChange", value);
}

protected void emitOnSubmit(ReadableArray value) {
mEventEmitterCallback.invoke("onSubmit", value);
}

@Override
public @Nonnull String getName() {
return NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@ NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(
1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectAssert};
methodMap_["promiseAssert"] = MethodMetadata{
0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseAssert};
eventEmitterMap_["onPress"] =
std::make_shared<AsyncEventEmitter<folly::dynamic>>();
eventEmitterMap_["onClick"] =
std::make_shared<AsyncEventEmitter<folly::dynamic>>();
eventEmitterMap_["onChange"] =
std::make_shared<AsyncEventEmitter<folly::dynamic>>();
eventEmitterMap_["onSubmit"] =
std::make_shared<AsyncEventEmitter<folly::dynamic>>();
setEventEmitterCallback(params.instance);
}

std::shared_ptr<TurboModule> SampleTurboModuleSpec_ModuleProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,26 @@ public double getRootTag(double arg) {
@Override
public void voidFunc() {
log("voidFunc", "<void>", "<void>");
return;
emitOnPress();
emitOnClick("click");
{
WritableNativeMap map = new WritableNativeMap();
map.putInt("a", 1);
map.putString("b", "two");
emitOnChange(map);
}
{
WritableNativeArray array = new WritableNativeArray();
WritableNativeMap map = new WritableNativeMap();
map.putInt("a", 1);
map.putString("b", "two");
array.pushMap(map);
WritableNativeMap map1 = new WritableNativeMap();
map1.putInt("a", 3);
map1.putString("b", "four");
array.pushMap(map1);
emitOnSubmit(array);
}
}

// This function returns {@link WritableMap} instead of {@link Map} for backward compat with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import type {
RootTag,
TurboModule,
} from '../../../../Libraries/TurboModule/RCTExport';
import type {UnsafeObject} from '../../../../Libraries/Types/CodegenTypes';
import type {
EventEmitter,
UnsafeObject,
} from '../../../../Libraries/Types/CodegenTypes';

import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry';

Expand All @@ -21,7 +24,17 @@ export enum EnumInt {
B = 42,
}

export type ObjectStruct = {
a: number,
b: string,
c?: ?string,
};

export interface Spec extends TurboModule {
+onPress: EventEmitter<void>;
+onClick: EventEmitter<string>;
+onChange: EventEmitter<ObjectStruct>;
+onSubmit: EventEmitter<ObjectStruct[]>;
// Exported methods.
+getConstants: () => {|
const1: boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import type {RootTag} from 'react-native/Libraries/ReactNative/RootTag';
import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';

import styles from './TurboModuleExampleCommon';
import * as React from 'react';
Expand Down Expand Up @@ -68,6 +69,7 @@ type ErrorExamples =

class SampleTurboModuleExample extends React.Component<{||}, State> {
static contextType: React$Context<RootTag> = RootTagContext;
eventSubscriptions: EventSubscription[] = [];

state: State = {
testResults: {},
Expand Down Expand Up @@ -218,6 +220,30 @@ class SampleTurboModuleExample extends React.Component<{||}, State> {
'The JSI bindings for SampleTurboModule are not installed.',
);
}
this.eventSubscriptions.push(
NativeSampleTurboModule.onPress(value => console.log('onPress: ()')),
);
this.eventSubscriptions.push(
NativeSampleTurboModule.onClick(value =>
console.log(`onClick: (${value})`),
),
);
this.eventSubscriptions.push(
NativeSampleTurboModule.onChange(value =>
console.log(`onChange: (${JSON.stringify(value)})`),
),
);
this.eventSubscriptions.push(
NativeSampleTurboModule.onSubmit(value =>
console.log(`onSubmit: (${JSON.stringify(value)})`),
),
);
}

componentWillUnmount() {
for (const subscription of this.eventSubscriptions) {
subscription.remove();
}
}

render(): React.Node {
Expand Down

0 comments on commit 7b6ffe6

Please sign in to comment.