feat(android): support Activity Result API for native modules - #57798
feat(android): support Activity Result API for native modules#57798matinzd wants to merge 10 commits into
Conversation
|
This is my decisions and how I planned implementing this with Claude. Comments are welcome! https://gist.github.com/matinzd/5cac3ef1811efc9817f055b8d27692f7 |
|
Not sure how to update |
|
@matinzd we can generate it after importing, I don't believe there's a public Gradle script for it |
Yep. I saw the command in other PRs using buck2! The PR is now ready to review. Can you please approve the jobs to run? |
|
Thanks for tackling this @matinzd, the ActivityResultContract gap is real and Health Connect is a good example. A few things I think are worth sorting first, mostly around lifecycle and threading, though I might be missing some context:
On testing, it looks like the current coverage is mostly the sample module wiring, so the lifecycle and threading paths above are not really exercised. It would help to add tests for those paths, mainly the rebind case (a launch after an Activity swap should hit the new registry) and the main-thread requirement for register and launch. |
|
Thanks for the review @fabriziocucci!
Sorry, that slipped through. Claude made a mistake, and it’s good that you caught it. I’m thinking of deriving the key from the FQCN of the native module combined with the contract FQCN. We could throw an error if the same contract is registered twice from the same module, prompting users to use the overload that accepts a caller-provided key instead. E.g: // Throws: MyModule already registered a launcher for androidx...GetContent.
private val pickAvatar = ctx.registerForActivityResult(this, GetContent()) { }
private val pickBanner = ctx.registerForActivityResult(this, GetContent()) { }
// Fix:
private val pickAvatar = ctx.registerForActivityResult("avatar", GetContent()) { }
private val pickBanner = ctx.registerForActivityResult("banner", GetContent()) { }or we can just silently add an index based key based on each module to avoid collision for e.g: // key = "com.some.image.lib.ImageModule:androidx...GetContent#0"
// key = "com.some.image.lib.ImageModule:androidx...GetContent#1"What do you think? (Not sure who to tag though from the core team)
For the rebind case, I tried adding a For the rest, I need to create an example app in order to test those cases. I will let you know when that's ready. |
You are right.
Probably worths using UPDATE 1 I implemented a fix for the thread-safety and rebinding issue. I tried to test it but it's hard to reproduce. I got some help from Claude to write some tests: f3d3fef To be honest, some parts are beyond my knowledge. The repo is too huge to understand everything, like what's running on which thread, but I tried my best to figure it out with the JDB. Hope this makes more sense now. |
…ntext Adds com.facebook.react.activityresult with a ReactActivityResultCaller that registers AndroidX ActivityResultContracts against the host Activity's own ActivityResultRegistry (ReactActivity already extends ComponentActivity, so it is an ActivityResultRegistryOwner). No changes to consumers' MainActivity, no manifest entries, no forked registry. - Registration is legal at any time: modules are created lazily, so the returned launcher is a deferred wrapper that binds to the registry on onHostResume, queues a single launch issued while unbound, and rebinds under the same key after Activity recreation. - Keys are the contract's fully-qualified class name; duplicate registrations throw at registration time, with an owner-scoped overload as the escape hatch. - ReactContext gains registerForActivityResult convenience methods mirroring ComponentActivity, plus getActivityResultCaller(). - ActivityEventListener dispatch is untouched; results flow through ComponentActivity's existing onActivityResult / onRequestPermissionsResult into its registry. Demo: SampleTurboModule.requestSamplePermission (CAMERA) wired into rn-tester's SampleTurboModuleExample under an Android-only section. Co-Authored-By: Claude Opus 5 <[email protected]>
f3d3fef to
c37cb17
Compare
Summary:
Rendered readme can be found here.
Bare React Native has no way for a native module to use AndroidX
ActivityResultContracts. Modules are stuck withActivityEventListenerand self-assigned int request codes. On Android 14+ some contracts (e.g. Health Connect's permission contract) produce a synthetic intent that only anActivityResultRegistrycan service, so the classicstartActivityForResultpath fails withActivityNotFoundExceptionoutright.Libraries work around this by demanding glue code in the consumer's
MainActivity(e.g.HealthConnectPermissionDelegate.setPermissionDelegate(this)) or by shipping a transparentActivityin their manifest, which cuts against Google's single-activity guidance (matinzd/react-native-health-connect#266, #33639, #36377). Expo solved this withregisterActivityContracts; bare RN has no equivalent.ReactActivityalready extendsComponentActivity, so it already owns a realActivityResultRegistryand routes results into it. Core just needs to hand modules a path to that registry:Design notes:
ComponentActivity.registerForActivityResultand returns the realandroidx.activity.result.ActivityResultLauncher<I>. The one addition is a leadingownerargument, which scopes the registration key.onHostResumeand queues alaunch()issued while unbound.ReactActivity/ReactActivityDelegate/ReactDelegate, no new Gradle dependency, no manifest changes, no forked registry.ActivityEventListeneris untouched.Promise) died with the JS context.Demos:
SampleTurboModule.requestSamplePermission()(CAMERA), pluspickMediaandpickMultipleMedia(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
Test Plan:
./gradlew :packages:react-native:ReactAndroid:compileDebugKotlinand:compileDebugJavaWithJavacpass; codegen emits the sample module methods intoNativeSampleTurboModuleSpec.Example App Recording
Screen.Recording.2026-08-03.at.15.27.08.mov