Files
react-native/packages/rn-tester/js/examples/TurboModule/NativeSampleTurboModule.js
T
matinzd ec2b536e14 feat(android): support Activity Result API for native modules (#57798)
Summary:
Rendered readme can be found [here](https://github.com/matinzd/react-native/blob/feat/permission_contracts_android/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/activityresult/__docs__/README.md).

Bare React Native has no way for a native module to use AndroidX `ActivityResultContract`s. Modules are stuck with `ActivityEventListener` and self-assigned int request codes, and some contracts (e.g. Health Connect's permission contract) have no `startActivityForResult` equivalent at all. Calling `registerForActivityResult` on `getCurrentActivity()` instead is a dead end: the lifecycle-observing overload crashes with `LifecycleOwner ... is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.`, because AndroidX only allows it before the Activity is `STARTED` — and native modules are created lazily, long after that ([https://github.com/react/react-native/issues/33639](https://github.com/facebook/react-native/issues/33639)).

Libraries work around this by demanding glue code in the consumer's `MainActivity`: react-native-health-connect today requires every app to add `HealthConnectPermissionDelegate.setPermissionDelegate(this)`. The proposed alternative — shipping a transparent `Activity` in the library's manifest ([matinzd/react-native-health-connect#266](https://github.com/matinzd/react-native-health-connect/pull/266), still an unreleased PR) — cuts against Google's single-activity guidance ([https://github.com/react/react-native/issues/33639](https://github.com/facebook/react-native/issues/33639), [https://github.com/react/react-native/issues/36377](https://github.com/facebook/react-native/issues/36377)). Expo solved this with [`registerActivityContracts`](https://docs.expo.dev/modules/module-api/#registeractivitycontracts); bare RN has no equivalent.

`ReactActivity` already extends `ComponentActivity`, so it already owns a real `ActivityResultRegistry` and routes results into it. Core just needs to hand modules a path to that registry:

```kotlin
private val getContent = reactContext.registerForActivityResult(
    /* owner = */ this, ActivityResultContracts.GetContent()) { uri -> ... }

getContent.launch("image/*")
```

Design notes:

- API mirrors `ComponentActivity.registerForActivityResult` and returns the real `androidx.activity.result.ActivityResultLauncher<I>`. The one addition is a leading `owner` argument, which scopes the registration key.
- Modules register before an Activity exists (they are created lazily), so the returned launcher binds to the registry on `onHostResume` and queues a `launch()` issued while unbound.
- No changes to `ReactActivity`/`ReactActivityDelegate`/`ReactDelegate`, no new Gradle dependency, no manifest changes, no forked registry. `ActivityEventListener` is untouched.
- Known limitation: on process death, AndroidX redelivers the pending result under the same key, but the module's in-flight state (typically a `Promise`) died with the JS context.

Demos: `SampleTurboModule.requestSamplePermission()` (CAMERA), plus `pickMedia` and `pickMultipleMedia` (photo picker, single and multi select with a JS-controlled limit), surfaced in rn-tester's SampleTurboModule and PhotoPickerAndroid screens.

## Changelog:

[ANDROID] [ADDED] - Add support for Activity Result API for native modules

Pull Request resolved: https://github.com/react/react-native/pull/57798

Test Plan:
- `./gradlew :packages:react-native:ReactAndroid:compileDebugKotlin` and `:compileDebugJavaWithJavac` pass; codegen emits the sample module methods into `NativeSampleTurboModuleSpec`.
- Flow, ESLint, prettier, and ktfmt clean.

## Example App Recording

https://github.com/user-attachments/assets/63750917-2325-4613-9a0d-b7241ae026eb

Reviewed By: javache

Differential Revision: D115622269

Pulled By: Abbondanzo

fbshipit-source-id: ce623a84d3c5b29f1bae57c2c177517d0cd34190
2026-09-25 10:50:37 -07:00

83 lines
2.6 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {CodegenTypes, RootTag, TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export enum EnumInt {
A = 23,
B = 42,
}
type ObjectStruct = {
a: number,
b: string,
c?: ?string,
};
export interface Spec extends TurboModule {
readonly onPress: CodegenTypes.EventEmitter<void>;
readonly onClick: CodegenTypes.EventEmitter<string>;
readonly onChange: CodegenTypes.EventEmitter<ObjectStruct>;
readonly onSubmit: CodegenTypes.EventEmitter<ObjectStruct[]>;
readonly getConstants: () => {
const1: boolean,
const2: number,
const3: string,
};
readonly voidFunc: () => void;
readonly getBool: (arg: boolean) => boolean;
readonly getEnum?: (arg: EnumInt) => EnumInt;
readonly getNumber: (arg: number) => number;
readonly getString: (arg: string) => string;
readonly getArray: (arg: Array<unknown>) => Array<unknown>;
readonly getObject: (
arg: CodegenTypes.UnsafeObject,
) => CodegenTypes.UnsafeObject;
readonly getUnsafeObject: (
arg: CodegenTypes.UnsafeObject,
) => CodegenTypes.UnsafeObject;
readonly getRootTag: (arg: RootTag) => RootTag;
readonly getValue: (
x: number,
y: string,
z: CodegenTypes.UnsafeObject,
) => CodegenTypes.UnsafeObject;
readonly getArrayBuffer: (buffer: ArrayBuffer) => ArrayBuffer;
readonly createNativeBuffer: (size: number) => ArrayBuffer;
readonly processAsyncBuffer: (payload: ArrayBuffer) => Promise<number>;
readonly getValueWithCallback: (callback: (value: string) => void) => void;
readonly getValueWithPromise: (error: boolean) => Promise<string>;
readonly voidFuncThrows?: () => void;
readonly getObjectThrows?: (
arg: CodegenTypes.UnsafeObject,
) => CodegenTypes.UnsafeObject;
readonly promiseThrows?: () => Promise<void>;
readonly voidFuncAssert?: () => void;
readonly getObjectAssert?: (
arg: CodegenTypes.UnsafeObject,
) => CodegenTypes.UnsafeObject;
readonly promiseAssert?: () => Promise<void>;
readonly getImageUrl?: () => Promise<string | null>;
readonly requestSamplePermission?: () => Promise<boolean>;
readonly pickMedia?: (mimeType: ?string) => Promise<string | null>;
readonly pickMultipleMedia?: (
mimeType: ?string,
maxItems: number,
) => Promise<Array<string>>;
readonly startSecondActivity?: () => void;
}
const RNTesterSampleTurboModule: Spec =
TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
export default RNTesterSampleTurboModule;