Skip to content

Commit

Permalink
Add Java Turbo Module Event Emitter example (#44906)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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

fbshipit-source-id: 04261d8885760f0e3b3c8c1931e0d56a5d33a0df
  • Loading branch information
christophpurrer authored and facebook-github-bot committed Jun 28, 2024
1 parent dba25fa commit 84a9f5e
Show file tree
Hide file tree
Showing 16 changed files with 131 additions and 21 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 @@ -652,7 +652,7 @@ export interface Spec extends TurboModule {
+onEvent6: EventEmitter<ObjectStruct[]>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModuleCxx');
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1531,11 +1531,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_EVENT_EM
],
'methods': []
},
'moduleName': 'SampleTurboModuleCxx',
'excludedPlatforms': [
'iOS',
'android'
]
'moduleName': 'SampleTurboModule'
}
}
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,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 @@ -751,7 +751,7 @@ export interface Spec extends TurboModule {
readonly onEvent6: EventEmitter<ObjectStruct[]>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModuleCxx');
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1731,11 +1731,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_EV
],
'methods': []
},
'moduleName': 'SampleTurboModuleCxx',
'excludedPlatforms': [
'iOS',
'android'
]
'moduleName': 'SampleTurboModule'
}
}
}"
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ public abstract class com/facebook/react/bridge/BaseJavaModule : com/facebook/re
public static final field METHOD_TYPE_ASYNC Ljava/lang/String;
public static final field METHOD_TYPE_PROMISE Ljava/lang/String;
public static final field METHOD_TYPE_SYNC Ljava/lang/String;
protected field mEventEmitterCallback Lcom/facebook/react/bridge/CxxCallbackImpl;
public fun <init> ()V
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
public fun canOverrideExistingModule ()Z
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";

protected @Nullable 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 @@ -40,6 +40,22 @@ public NativeSampleTurboModuleSpec(ReactApplicationContext reactContext) {
return NAME;
}

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);
}

protected abstract Map<String, Object> getTypedExportedConstants();

@Override
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 84a9f5e

Please sign in to comment.