All posts
Platform Deep Dives7 min read

Android back gesture handling in Flutter vs native. The edge cases nobody talks about.

Predictive back, PopScope, GoRouter, and the bottom sheet that swallows your gesture. The Android back gesture story for Flutter, with Kotlin equivalents.

A QA engineer once filed a bug that read: "swipe back from bottom sheet does nothing." It took me a day to realize the bottom sheet was eating the horizontal pan, the navigator was not getting the gesture, and on Android 14 the system was sending a predictive back animation that we were ignoring entirely. There were three layers of behaviour and none of them were on the same page.

If you have ever told a junior to "just use WillPopScope" you owe them an apology, because that class is deprecated and the replacement, PopScope, behaves differently in subtle ways. This post is the version of the story that covers all the edge cases: predictive back, the deprecation, GoRouter integration, and the gesture conflicts that nobody mentions until production.

Context: what the platform now does

Android 13 introduced a predictive back gesture API. The system shows a preview of the destination during the gesture, and an app must opt in to participate. Android 14 made it the default for apps that target API 34. Android 15 continues the rollout. If you do not opt in, you get the legacy behaviour and your app looks dated next to the system apps.

Flutter has caught up. Flutter 3.16 deprecated WillPopScope and introduced PopScope. Flutter 3.27 added a PredictiveBackPageTransitionsBuilder that participates in the system animation. The pieces are there. They do not assemble themselves.

PopScope, the actual replacement

The behavioural difference: WillPopScope returned a Future<bool> that the system awaited before popping. PopScope declares up-front whether the route can pop (canPop), and your callback runs after the system has already decided. If you set canPop: false, the route never pops by gesture; you must call Navigator.pop yourself when you want it to.

The reason for the change: predictive back. The system needs to know during the gesture whether your screen will pop, so it can render the right preview. WillPopScope could only answer after the user lifted their finger, which is too late.

onPopInvokedWithResult was introduced in Flutter 3.22 and is preferred over the older onPopInvoked callback because it surfaces the optional result value from the pop.

Kotlin equivalent

Notice that the Android API exposes the in-progress events too. Flutter does not surface the gesture progress directly to your callback; it animates the route transition itself when you wire up the predictive back transitions builder.

Wiring up predictive back in Flutter

That single change opts your app into the system animation on Android. You also need:

  • targetSdk 34 or higher in android/app/build.gradle.
  • android:enableOnBackInvokedCallback="true" on the <application> tag in AndroidManifest.xml (required for system-driven predictive back).

Without those, the transitions builder will be a no-op on real devices.

GoRouter integration

GoRouter sits between your widget tree and the navigator. PopScope still works with GoRouter, but you must understand the route hierarchy.

The trap: if you call context.go('/') instead of context.pop(), you bypass the navigator's pop logic and the system back-gesture preview targets a destination that is not where you sent the user. Always pop when responding to back; only go when responding to an explicit user action like a "Cancel" button.

The bottom sheet gesture conflict

A modal bottom sheet from showModalBottomSheet registers its own drag-to-dismiss gesture. When the system-level back gesture begins from the left edge, the OS handles it. When the user drags down on the sheet, the sheet handles it. When the user starts a horizontal swipe with their finger over the sheet content, both want it.

Flutter's gesture arena resolves this by giving the win to the closest gesture in the tree. The sheet wins because it is on top. The result: edge swipes that originate over a bottom sheet sometimes get absorbed.

The fix is to wrap your sheet content so non-vertical gestures pass through:

This is not a perfect fix. Once the system gesture starts from the edge, Flutter does not see it at all on Android 14+ because the system handles edge inputs. The conflict shows up most for swipes that begin further inboard, where the OS hands the gesture to your app.

Sequence diagram of the back gesture event flow

Caption: the back gesture flow when predictive back is enabled. The critical handoff is canPop, which the system needs synchronously during the gesture.

What I would do differently

  • I would have migrated off WillPopScope the day Flutter 3.16 deprecated it. Living on it for a year cost me three rewrites later.
  • I would have enabled predictive back the same day I bumped to targetSdk 34. Doing them in separate sprints created a window where the manifest flag was off and I did not know.
  • I would have written one widget test per PopScope from the start. Pop logic is the most regression-prone code I own.
  • I would have stopped using nested Navigators for tab bars. The pop behaviour with the system gesture is hard to reason about. A single navigator with a shell route in GoRouter is cleaner.
  • I would have read the Android OnBackPressedCallback source to understand the dispatcher. Five minutes there explains a week of mysterious bugs.

Closing opinion

Migrate to PopScope, enable predictive back transitions, and stop nesting navigators inside tabs. If you only do those three things, you will fix the majority of the back-gesture bugs in your app. For the broader gesture system comparison, see Flutter gesture system vs UIGestureRecognizer. For platform-channel work that often gets confused with this, see Writing a Flutter platform channel in Swift.

Written by the author of Flutterstacks

A developer who shipped production apps in Swift, Kotlin, and Dart — with a genuine native reference point that most Flutter writers simply don't have.

More articles →

Continue reading

You may also enjoy

Platform Deep Dives

Writing a Flutter platform channel in Swift. What the official docs skip.

A complete walk through a production platform channel in Swift, including threading, error propagation, and passing complex objects. The parts the docs gloss over.

Read article
Platform Deep Dives

Background processing in Flutter. What a native developer needs to know.

Isolates, WorkManager on Android, BGTaskScheduler on iOS, and the places where Flutter's abstraction breaks down. Native Kotlin and Swift equivalents included.

Read article