Description
On Android, a TextInput that lives on a screen pushed onto a native stack never gets an autofill session. The platform does not merely fail to show a suggestion: it never sends a FillRequest to the autofill service at all, so no password manager can offer credentials, and no "save password" prompt appears after a successful sign-in.
The same TextInput, with the same props, works normally on the stack's initial route. That is the whole difference.
This is what makes it hard to spot from the app side: the fields are correctly annotated, the accessibility tree is correct, the keyboard opens, and nothing is logged. The framework simply never asks.
Verified with two different autofill services (Bitwarden and Google's), so this is not service-specific. Measured with adb shell dumpsys autofill, section Requests history:
| Screen |
FillRequest recorded |
| initial route of the stack |
yes, immediately on focus |
| route pushed onto the same stack |
none, ever |
A native Android app on the same emulator, with the same service, records a request instantly, so the device and the service are working.
Expected: focusing an autofillable TextInput starts an autofill session, wherever the screen sits in the navigation stack.
Things that do not change the outcome
What does change it
Replacing <Stack> with expo-router's <Slot>, which builds its navigation with the same StackRouter but renders only the focused route, without the native screen container. Autofill then works on every screen. That is the workaround we shipped, and it is only viable because those screens needed no stack animation.
Hypothesis
Offered as a hypothesis, not a claim, since I have not instrumented the native side.
View.notifyEnterOrExitForAutoFillIfNeeded defers the autofill notification to the next View.layout() when isLaidOut() is false, setting PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT instead of calling AutofillManager.notifyViewEntered. For a pushed screen, the subtree appears to be mounted and laid out by the Fabric mount batch before the fragment transaction attaches it, and View.onAttachedToWindow clears that laid-out flag. React Native never lays out a view again when its frame has not changed, so the deferred notification never fires.
That would explain why the initial route is unaffected: its fragment is attached before the first layout, not after it.
Steps to reproduce
- Create a blank Expo SDK 54 app and add
expo-router.
- Use the three files below.
npx expo prebuild --platform android and build a release APK.
- Install a password manager (Bitwarden works) and set it as the system autofill service, or use Google's.
- Launch the app. On the initial screen, focus a field.
adb shell dumpsys autofill shows a new line under Requests history.
- Tap "Go to sign in" to push the second screen. Focus the username field.
adb shell dumpsys autofill shows no new line. No suggestion appears above the keyboard.
To see the contrast in one run, put a TextInput on both screens.
app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
app/index.tsx
import { Link } from "expo-router";
import { Text, View } from "react-native";
export default function Index() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", gap: 24 }}>
<Text style={{ fontSize: 20 }}>Autofill repro</Text>
<Link href="/login" style={{ fontSize: 18, color: "#06c", padding: 16 }}>
Go to sign in
</Link>
</View>
);
}
app/login.tsx
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<View style={styles.page}>
<Text style={styles.h1}>Sign in</Text>
<TextInput
style={styles.input}
value={username}
onChangeText={setUsername}
placeholder="username"
autoComplete="username"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="password"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
/>
</View>
);
}
const styles = StyleSheet.create({
page: { flex: 1, paddingTop: 120, paddingHorizontal: 20, gap: 16, backgroundColor: "#fff" },
h1: { fontSize: 22, fontWeight: "700", marginBottom: 12 },
input: { borderWidth: 1, borderColor: "#bbb", borderRadius: 8, paddingHorizontal: 12, height: 48, fontSize: 16 },
});
Snack or a link to a repository
https://github.com/tomKFM/rn-screens-android-autofill-repro
Screens version
4.16.0
React Native version
0.81.5
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo bare workflow
Build type
Release mode
Device
Android emulator
Device model
Pixel emulator, Android 17 (API 37), also seen on a production build on physical devices
Acknowledgements
Yes
Related
Description
On Android, a
TextInputthat lives on a screen pushed onto a native stack never gets an autofill session. The platform does not merely fail to show a suggestion: it never sends aFillRequestto the autofill service at all, so no password manager can offer credentials, and no "save password" prompt appears after a successful sign-in.The same
TextInput, with the same props, works normally on the stack's initial route. That is the whole difference.This is what makes it hard to spot from the app side: the fields are correctly annotated, the accessibility tree is correct, the keyboard opens, and nothing is logged. The framework simply never asks.
Verified with two different autofill services (Bitwarden and Google's), so this is not service-specific. Measured with
adb shell dumpsys autofill, sectionRequests history:FillRequestrecordedA native Android app on the same emulator, with the same service, records a request instantly, so the device and the service are working.
Expected: focusing an autofillable
TextInputstarts an autofill session, wherever the screen sits in the navigation stack.Things that do not change the outcome
importantForAutofill="yes"on the field.animation: "none"and the default are both affected).headerversusheaderShown: false.What does change it
Replacing
<Stack>with expo-router's<Slot>, which builds its navigation with the sameStackRouterbut renders only the focused route, without the native screen container. Autofill then works on every screen. That is the workaround we shipped, and it is only viable because those screens needed no stack animation.Hypothesis
Offered as a hypothesis, not a claim, since I have not instrumented the native side.
View.notifyEnterOrExitForAutoFillIfNeededdefers the autofill notification to the nextView.layout()whenisLaidOut()is false, settingPFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUTinstead of callingAutofillManager.notifyViewEntered. For a pushed screen, the subtree appears to be mounted and laid out by the Fabric mount batch before the fragment transaction attaches it, andView.onAttachedToWindowclears that laid-out flag. React Native never lays out a view again when its frame has not changed, so the deferred notification never fires.That would explain why the initial route is unaffected: its fragment is attached before the first layout, not after it.
Steps to reproduce
expo-router.npx expo prebuild --platform androidand build a release APK.adb shell dumpsys autofillshows a new line underRequests history.adb shell dumpsys autofillshows no new line. No suggestion appears above the keyboard.To see the contrast in one run, put a
TextInputon both screens.app/_layout.tsxapp/index.tsxapp/login.tsxSnack or a link to a repository
https://github.com/tomKFM/rn-screens-android-autofill-repro
Screens version
4.16.0
React Native version
0.81.5
Platforms
Android
JavaScript runtime
Hermes
Workflow
Expo bare workflow
Build type
Release mode
Device
Android emulator
Device model
Pixel emulator, Android 17 (API 37), also seen on a production build on physical devices
Acknowledgements
Yes
Related