●DEADLINE — From August 31, every new app and app update on Google Play must target Android 16 (API level 36). Eight days to go●EXTENSION — Eligible developers can request an extension through November 1 via a form in Play Console, but the request itself has to be filed before the deadline●EXISTING — Even apps you leave alone need at least API level 35 to stay discoverable to new users on recent Android devices. Standing still quietly cuts off new installs●EXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 as a small, one-command upgrade with no breaking changes. React stays at 19.2, same as SDK 56●MEMORY — expo@57.0.9 bumps React Native to 0.86.2 and clears the Hermes V1 memory regression that inflated usage in apps importing reanimated or worklets●PREBUILD — expo prebuild now clears and regenerates the native directories by default, which collides easily with hand-edited config from your API 36 migration●DEADLINE — From August 31, every new app and app update on Google Play must target Android 16 (API level 36). Eight days to go●EXTENSION — Eligible developers can request an extension through November 1 via a form in Play Console, but the request itself has to be filed before the deadline●EXISTING — Even apps you leave alone need at least API level 35 to stay discoverable to new users on recent Android devices. Standing still quietly cuts off new installs●EXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 as a small, one-command upgrade with no breaking changes. React stays at 19.2, same as SDK 56●MEMORY — expo@57.0.9 bumps React Native to 0.86.2 and clears the Hermes V1 memory regression that inflated usage in apps importing reanimated or worklets●PREBUILD — expo prebuild now clears and regenerates the native directories by default, which collides easily with hand-edited config from your API 36 migration
Predictive back on Android 16: what to fix before August 31, and what can wait
Targeting API 36 turns predictive back on by default and stops onBackPressed from firing. Here is how I split the work that the deadline actually requires from the work that does not, and how I kept the opt-out from disappearing on the next prebuild.
After bumping targetSdkVersion to 36, the preview screen looked different when I swiped in from the edge. The screen shrank slightly and revealed what was behind it. That is Android's predictive back animation.
That part I expected. What stopped me was what came next. A swipe I meant as "go back to the grid" was being treated as "leave the app."
The React Native announcement says BackHandler keeps working. And it did keep working. Yet the outcome of the back gesture had changed. It took me a while to understand how both of those can be true at the same time.
"BackHandler still works" and "back navigation is unchanged" are different claims
React Native 0.81 absorbed that change so the JS-side BackHandler keeps firing. The community announcement states that onBackPressed is no longer called, that BackHandler should continue to work, and that if you have custom native back handling you may need to migrate it by hand to OnBackPressedDispatcher.
What I misread was the scope of "BackHandler still works." What keeps working is the handler you registered yourself on hardwareBackPress. Screens where you did not register anything — the ones that hand back navigation to your navigation library — are not covered by that promise.
And React Navigation has not caught up with predictive back yet. Its documentation currently tells you to set android:enableOnBackInvokedCallback to false so the system back gesture behaves as expected. On the Expo side there are reports of stack back navigation breaking under predictive back, with work happening in the newer native stack in react-native-screens.
So the thing that breaks is not the BackHandler you wrote. It is the code you never wrote. That was the most counterintuitive part of this for me: rereading my own source gives you no reason for the failure.
What the opt-out flag stops, and what it does not
android:enableOnBackInvokedCallback="false" is the temporary opt-out Google documents. Its effect is asymmetric in a way that matters for planning.
Target
With the flag set to false
Predictive back system animation
Disabled
OnBackInvokedCallback
Ignored
OnBackPressedCallback
Still called, regardless of the flag
OnBackPressedCallback fires either way. Anything handling back through the AndroidX APIs behaves the same with or without the opt-out. What the flag rolls back is the system animation and the newer callback path.
One more detail. The attribute can live on <application> or on <activity>, and the per-activity value wins. If you set false on <application> but an old true is still sitting on an <activity> from some past experiment, that one screen will not match the rest. I decided to catch that mechanically rather than by memory, which is why the script below inspects both elements.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Decide with confidence that the August 31 deadline only requires the target SDK bump, and that full predictive-back support can be scheduled separately
✦Sort which of your apps can actually break on back navigation using a script, before you spend an evening tapping through screens on a device
✦Keep an opt-out from silently vanishing on the next prebuild, so a fix you already made does not quietly revert
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
This is the actual decision. What Google Play requires on August 31 is that the bundle you submit targets API level 36. It does not require that your app fully supports predictive back.
Treat those as one task and you can burn every remaining day on a navigation redesign without shipping. That is roughly how I had scoped it at first.
Work
Before August 31
Can it wait?
Raise targetSdkVersion to 36
Required
No
Handle edge-to-edge insets
Required
No — the layout visibly breaks
Add the opt-out to keep the old back behavior
Recommended
— it takes minutes
Real predictive-back navigation support
Not required
Yes
The opt-out is not a permanent answer. Google frames it as temporary, and you should assume a future release removes it. Even so, you can avoid cramming a hard deadline and an open-ended UX improvement into the same week.
I made the split on reversibility. Missing the deadline means you cannot ship updates at all, and there is no way back from that. Shipping with the old back animation costs you something you can recover later. When one side is irreversible and the other is not, I clear the irreversible one first.
Narrow the target with a script before touching a device
With several apps in flight, walking every screen on a device is not realistic. In my case the only places that intercept back are the full-screen preview and the modals; everything else defers to navigation. So the apps worth looking at first are the ones that use BackHandlerand have no opt-out in the manifest.
I wrote that triage as a small Node script. Pass it your app roots.
// audit-back.mjs — cross-check back handling in source against the manifestimport { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';import { join } from 'node:path';const roots = process.argv.slice(2);function walk(dir, out = []) { if (!existsSync(dir)) return out; for (const name of readdirSync(dir)) { // skip generated and vendored trees; android/ios are read separately below if (name === 'node_modules' || name === '.git' || name === 'android' || name === 'ios') continue; const p = join(dir, name); if (statSync(p).isDirectory()) walk(p, out); else if (/\.(ts|tsx|js|jsx)$/.test(name)) out.push(p); } return out;}// application and activity carry different meaning, so report them separatelyfunction readManifestFlag(manifestPath) { if (!existsSync(manifestPath)) return { exists: false }; const xml = readFileSync(manifestPath, 'utf8'); const pick = (tag) => { // match a single opening tag, even when attributes wrap across lines const m = xml.match(new RegExp(`<${tag}\\b[^>]*>`, 's')); if (!m) return null; const attr = m[0].match(/android:enableOnBackInvokedCallback\s*=\s*"(true|false)"/); return attr ? attr[1] : null; }; return { exists: true, application: pick('application'), activity: pick('activity') };}for (const root of roots) { const files = walk(join(root, 'src')); const backHandlerFiles = files.filter((f) => /BackHandler/.test(readFileSync(f, 'utf8'))); const f = readManifestFlag(join(root, 'android/app/src/main/AndroidManifest.xml')); let state; if (!f.exists) state = 'no manifest (not prebuilt yet)'; else if (f.application === 'false' || f.activity === 'false') state = 'opt-out present'; else if (f.application === 'true' || f.activity === 'true') state = 'explicitly opted in'; else state = 'unset (predictive back is on by default at API 36)'; const risk = backHandlerFiles.length > 0 && state.startsWith('unset') ? 'REVIEW' : 'ok'; console.log( `${risk.padEnd(6)} ${root.split('/').pop().padEnd(14)} ` + `BackHandler=${String(backHandlerFiles.length).padStart(2)} manifest=${state}` );}
Running it against three sample projects I set up gives this:
REVIEW wallpaper-a BackHandler= 1 manifest=unset (predictive back is on by default at API 36)ok wallpaper-b BackHandler= 1 manifest=opt-out presentok wallpaper-c BackHandler= 0 manifest=explicitly opted in
Check only the REVIEW rows on a device and the surface area drops sharply. An ok row does not mean "never look at it" — it means "nothing in this change gives it a new way to break."
If a project has no android/ directory yet, you will see no manifest. For those, the place to set the flag is not the manifest but the config plugin below.
Do not hand-edit the manifest
Here is the trap. If you edit AndroidManifest.xml directly to add the opt-out, expo prebuild will erase it. In SDK 57 the default changed: prebuild now discards and regenerates the android and ios directories. Preserving hand edits requires passing --no-clean.
That default lands badly during a deadline week. I did not want a fix that quietly reverts on the next build, so I kept the manifest untouched and put the change in a config plugin instead. Taking stock of your native drift before that upgrade is worth doing on its own (Find the native edits expo prebuild will erase before you upgrade to SDK 57).
The plugin looks like this.
// plugins/with-back-invoked-optout.jsconst { withAndroidManifest } = require('@expo/config-plugins');// isolate the part that mutates the manifest's JSON (xml2js) representationfunction setEnableOnBackInvokedCallback(androidManifest, value) { const app = androidManifest.manifest.application?.[0]; if (!app) throw new Error('no application element found'); app.$['android:enableOnBackInvokedCallback'] = String(value); // per-activity values win over application, so a leftover one desyncs that screen for (const activity of app.activity ?? []) { delete activity.$['android:enableOnBackInvokedCallback']; } return androidManifest;}module.exports = function withBackInvokedOptOut(config, { enabled = false } = {}) { return withAndroidManifest(config, (config) => { config.modResults = setEnableOnBackInvokedCallback(config.modResults, enabled); return config; });};module.exports.setEnableOnBackInvokedCallback = setEnableOnBackInvokedCallback;
The mutation lives in its own exported function so you can test it. Call it directly, without going through @expo/config-plugins, and hand it a manifest object you built yourself.
const manifest = { manifest: { application: [ { $: { 'android:name': '.MainApplication' }, activity: [ { $: { 'android:name': '.MainActivity', 'android:enableOnBackInvokedCallback': 'true' } }, ], }, ], },};setEnableOnBackInvokedCallback(manifest, false);// application: {"android:name":".MainApplication","android:enableOnBackInvokedCallback":"false"}// activity : {"android:name":".MainActivity"}// prebuild runs more than once, so confirm a second pass does not corrupt anythingsetEnableOnBackInvokedCallback(manifest, false);// application: {"android:name":".MainApplication","android:enableOnBackInvokedCallback":"false"}
That one line clearing the activity attribute exists because of the precedence rule from earlier. Leave it out and you get the hard-to-trace state where <application> says false but a single screen behaves differently. The symptom reads as "back is weird on this one screen," which is a miserable thing to debug from a user report.
When you are ready to drop the opt-out, flip enabled to true. Keeping it as a plugin also leaves a record in the repo of when and why it went in, which your future self will appreciate.
On a device, only check the places that intercept back
For the app flagged REVIEW, I looked at three things. All of them are places that hold onto back themselves.
Returning from the preview screen — going from a full-screen wallpaper back to the grid. If this exits the app, it is the most jarring failure of the three
Modals and bottom sheets — whether back closes the sheet or pops the screen. When more than one component tracks presentation state, they fight over it here
The exit confirmation — if your root screen returns true from a handler to show a confirm dialog, check that branch still runs
I use a physical device rather than an emulator because gesture handling varies by manufacturer, and because three-button navigation and gesture navigation present differently — something that is easy to miss without hardware in hand.
Where to start
Open AndroidManifest.xml. Not app.json, not build.gradle.
If enableOnBackInvokedCallback appears on neither <application> nor <activity>, then the moment you target API level 36 your back navigation switched to the new path. The build succeeds, BackHandler runs, and Play Console says nothing. The only thing that changed is what happens when a user swipes.
Meet the deadline with the bump; rebuild the back experience afterward. But knowing the switch happened, rather than discovering it from a one-star review, changes how you read the next batch of feedback.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.