Skip to content

Commit 957e0b5

Browse files
committed
docs: add readme
1 parent 91bc282 commit 957e0b5

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

  • packages/react-native/ReactAndroid/src/main/java/com/facebook/react/activityresult/__docs__
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# ActivityResultContracts for native modules
2+
3+
[🏠 Home](../../../../../../../../../../../__docs__/README.md)
4+
5+
This package lets an Android native module register an AndroidX
6+
[`ActivityResultContract`](https://developer.android.com/training/basics/intents/result)
7+
against the host Activity's `ActivityResultRegistry` and receive results, with
8+
no changes to the consumer app's `MainActivity`, no manifest entries, and no
9+
library-shipped transparent Activities.
10+
11+
Before this existed, modules had to use `ActivityEventListener` with
12+
self-assigned int request codes: codes live in a global namespace with no
13+
coordination between libraries, results are broadcast so every listener
14+
filters, and intents are built and parsed by hand. On Android 14+ some
15+
contracts (e.g. Health Connect's permission contract) produce a synthetic
16+
intent that only an `ActivityResultRegistry` can service, so the classic
17+
`startActivityForResult` path fails with `ActivityNotFoundException` outright.
18+
19+
## 🚀 Usage
20+
21+
The API is `ReactContext.registerForActivityResult`, deliberately identical in
22+
shape to
23+
[`ComponentActivity.registerForActivityResult`](https://developer.android.com/training/basics/intents/result#register):
24+
same name, same `ActivityResultCallback<O>`, and it returns the real
25+
`androidx.activity.result.ActivityResultLauncher<I>`.
26+
27+
```kotlin
28+
class MyModule(private val context: ReactApplicationContext) :
29+
NativeMyModuleSpec(context) {
30+
31+
private var pendingPromise: Promise? = null
32+
33+
// Registering in a field initializer is fine: modules are created lazily,
34+
// long after the Activity exists, and registration is legal at any time.
35+
private val requestPermission =
36+
context.registerForActivityResult(
37+
ActivityResultContracts.RequestPermission()) { isGranted ->
38+
pendingPromise?.resolve(isGranted)
39+
pendingPromise = null
40+
}
41+
42+
override fun requestCameraPermission(promise: Promise) {
43+
pendingPromise = promise
44+
requestPermission.launch(Manifest.permission.CAMERA)
45+
}
46+
}
47+
```
48+
49+
Stock AndroidX contracts work unchanged, with their own input and output
50+
types:
51+
52+
```kotlin
53+
private val pickMedia =
54+
context.registerForActivityResult(
55+
ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
56+
// null when the user dismissed the picker
57+
}
58+
59+
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
60+
```
61+
62+
### Registration keys and collisions
63+
64+
The registration key is the contract's fully-qualified class name, derived by
65+
core and never passed by the caller. Registering the same contract class twice
66+
on one `ReactContext` throws `IllegalStateException` at registration time,
67+
naming both registrants. Two ways to disambiguate:
68+
69+
- **Subclass the contract** (preferred; see the parameterized-contract pattern
70+
below) — the subclass has its own class name and therefore its own key.
71+
- **Use the owner overload**
72+
`context.registerForActivityResult(owner, contract, callback)` scopes the
73+
key to `"<owner class>:<contract class>"`:
74+
75+
```kotlin
76+
private val getContent =
77+
context.registerForActivityResult(
78+
/* owner = */ this, ActivityResultContracts.GetContent()) { uri -> ... }
79+
```
80+
81+
### Parameterized contracts: passing values from JS per call
82+
83+
Contract constructor arguments are fixed at registration time. If a value
84+
comes from JS per call — say the photo picker's item limit — move it into the
85+
contract's **input** type, where it becomes a `launch()` argument. Subclass
86+
the stock contract and delegate:
87+
88+
```kotlin
89+
private class PickUpToMedia :
90+
ActivityResultContract<PickUpToMedia.Request, List<@JvmSuppressWildcards Uri>>() {
91+
class Request(val maxItems: Int, val request: PickVisualMediaRequest)
92+
93+
private val delegate = ActivityResultContracts.PickMultipleVisualMedia(2)
94+
95+
override fun createIntent(context: Context, input: Request): Intent =
96+
delegate.createIntent(context, input.request).apply {
97+
putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, input.maxItems)
98+
}
99+
100+
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> =
101+
delegate.parseResult(resultCode, intent)
102+
}
103+
104+
// One registration serves every limit JS asks for:
105+
launcher.launch(PickUpToMedia.Request(jsMaxItems, request))
106+
```
107+
108+
This is the pattern for *any* per-call parameter, and it doubles as the
109+
collision fix since the subclass gets a distinct key.
110+
111+
### Working examples
112+
113+
- `SampleTurboModule.kt`
114+
(`ReactCommon/react/nativemodule/samples/platform/android/`) —
115+
`requestSamplePermission` (runtime permission), `pickMedia` (photo picker,
116+
single select), `pickMultipleMedia` (multi select with a JS-controlled limit
117+
via the `PickUpToMedia` contract above).
118+
- rn-tester screens: `TurboModule/SampleTurboModuleExample.js` and
119+
`PhotoPickerAndroid/PhotoPickerAndroid.js`.
120+
121+
## 📐 Design
122+
123+
`ReactActivity` extends `ComponentActivity`, so the host Activity already owns
124+
a real `ActivityResultRegistry` and already routes `onActivityResult` /
125+
`onRequestPermissionsResult` into it. This package only bridges the timing gap
126+
between lazily-created modules and that registry — it does not fork or
127+
reimplement the registry.
128+
129+
- `ReactActivityResultCaller` / `ReactActivityResultCallerImpl` (internal):
130+
owned by the `ReactContext`, holds `(key, contract, callback)` registrations,
131+
and binds them to the current Activity's registry — immediately when an
132+
Activity is available, otherwise on the next `onHostResume`.
133+
- `DeferredActivityResultLauncher` (internal): the launcher handed to callers.
134+
Delegates to the real AndroidX launcher once bound; a `launch()` issued while
135+
unbound is queued (latest wins) and fired on bind.
136+
- On `onHostDestroy` registrations detach from the dying registry but are
137+
kept, and rebind against the new Activity's registry under the same keys on
138+
the next `onHostResume`. Stable keys are what let AndroidX re-associate a
139+
result that arrives after Activity recreation.
140+
141+
Behavioral notes for library authors:
142+
143+
- **Register early, ideally in a field initializer or the module
144+
constructor.** Registration is cheap and legal at any time; launching is
145+
what needs an Activity.
146+
- **An Activity that is not an `ActivityResultRegistryOwner`** (i.e. does not
147+
extend `ComponentActivity`) cannot service launchers; they stay queued and a
148+
warning is logged.
149+
- **Process death:** AndroidX redelivers a pending result under the same key
150+
after the process is recreated, but whatever state your module held for the
151+
in-flight call (typically a `Promise`) died with the JS context. Design
152+
callbacks to tolerate firing with no pending state.
153+
- **`unregister()`** on the returned launcher removes the registration; the
154+
same contract class can then be registered again.
155+
156+
## 🔗 Relationship with other systems
157+
158+
### Part of
159+
160+
- [ReactAndroid](../../../../../../../../README.md) — the core of React
161+
Native on Android.
162+
163+
### Used by this
164+
165+
- `com.facebook.react.bridge.ReactContext` — exposes the public
166+
`registerForActivityResult` methods and owns the caller instance; its
167+
`LifecycleEventListener` events (`onHostResume` / `onHostDestroy`) drive
168+
binding and rebinding.
169+
- AndroidX `androidx.activity.result` — the contracts, launchers, and the
170+
host Activity's `ActivityResultRegistry` that actually starts activities and
171+
dispatches results.
172+
173+
### Uses this
174+
175+
- `SampleTurboModule` (demo) and, prospectively, third-party native modules
176+
that need activity results or AndroidX permission contracts (e.g. Health
177+
Connect).
178+
179+
This API coexists with `ActivityEventListener`, which is unchanged: results
180+
claimed by the AndroidX registry are consumed by it, everything else still
181+
reaches `ActivityEventListener.onActivityResult`. The listener remains the
182+
right tool for intents a module builds and starts itself.

0 commit comments

Comments
 (0)