Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit c364b29

Browse files
authored
FlutterFragment predictive back (#52302)
Android add-to-app apps now support predictive back when going between Activities or back to the home screen. Predictive back route transitions within the Flutter part of the app are not yet supported.
1 parent 4681b09 commit c364b29

File tree

3 files changed

+47
-9
lines changed

3 files changed

+47
-9
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterFragment.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,14 @@ public void onAttach(@NonNull Context context) {
10561056
delegate.onAttach(context);
10571057
if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
10581058
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
1059+
// When Android handles a back gesture, it pops an Activity or goes back
1060+
// to the home screen. When Flutter handles a back gesture, it pops a
1061+
// route inside of the Flutter part of the app. By default, Android
1062+
// handles back gestures, so this callback is disabled. If, for example,
1063+
// the Flutter app has routes for which it wants to handle the back
1064+
// gesture, then it will enable this callback using
1065+
// setFrameworkHandlesBack.
1066+
onBackPressedCallback.setEnabled(false);
10591067
}
10601068
context.registerComponentCallbacks(this);
10611069
}
@@ -1663,16 +1671,29 @@ public boolean popSystemNavigator() {
16631671
// Unless we disable the callback, the dispatcher call will trigger it. This will then
16641672
// trigger the fragment's onBackPressed() implementation, which will call through to the
16651673
// dart side and likely call back through to this method, creating an infinite call loop.
1666-
onBackPressedCallback.setEnabled(false);
1674+
boolean enabledAtStart = onBackPressedCallback.isEnabled();
1675+
if (enabledAtStart) {
1676+
onBackPressedCallback.setEnabled(false);
1677+
}
16671678
activity.getOnBackPressedDispatcher().onBackPressed();
1668-
onBackPressedCallback.setEnabled(true);
1679+
if (enabledAtStart) {
1680+
onBackPressedCallback.setEnabled(true);
1681+
}
16691682
return true;
16701683
}
16711684
}
16721685
// Hook for subclass. No-op if returns false.
16731686
return false;
16741687
}
16751688

1689+
@Override
1690+
public void setFrameworkHandlesBack(boolean frameworkHandlesBack) {
1691+
if (!getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
1692+
return;
1693+
}
1694+
onBackPressedCallback.setEnabled(frameworkHandlesBack);
1695+
}
1696+
16761697
@VisibleForTesting
16771698
@NonNull
16781699
boolean shouldDelayFirstAndroidViewDraw() {

shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ protected FlutterFragment createFlutterFragment() {
518518
? TransparencyMode.opaque
519519
: TransparencyMode.transparent;
520520
final boolean shouldDelayFirstAndroidViewDraw = renderMode == RenderMode.surface;
521+
final boolean shouldAutomaticallyHandleOnBackPressed = true;
521522

522523
if (getCachedEngineId() != null) {
523524
Log.v(
@@ -542,6 +543,7 @@ protected FlutterFragment createFlutterFragment() {
542543
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
543544
.destroyEngineWithFragment(shouldDestroyEngineWithHost())
544545
.shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw)
546+
.shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed)
545547
.build();
546548
} else {
547549
Log.v(
@@ -577,6 +579,7 @@ protected FlutterFragment createFlutterFragment() {
577579
.transparencyMode(transparencyMode)
578580
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
579581
.shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw)
582+
.shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed)
580583
.build();
581584
}
582585

@@ -592,6 +595,7 @@ protected FlutterFragment createFlutterFragment() {
592595
.transparencyMode(transparencyMode)
593596
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
594597
.shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw)
598+
.shouldAutomaticallyHandleOnBackPressed(shouldAutomaticallyHandleOnBackPressed)
595599
.build();
596600
}
597601
}
@@ -616,12 +620,6 @@ protected void onNewIntent(@NonNull Intent intent) {
616620
super.onNewIntent(intent);
617621
}
618622

619-
@Override
620-
@SuppressWarnings("MissingSuperCall")
621-
public void onBackPressed() {
622-
flutterFragment.onBackPressed();
623-
}
624-
625623
@Override
626624
public void onRequestPermissionsResult(
627625
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ private FragmentActivity getMockFragmentActivity() {
290290
}
291291

292292
@Test
293-
public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() {
293+
public void itDelegatesOnBackPressedWithSetFrameworkHandlesBack() {
294294
// We need to mock FlutterJNI to avoid triggering native code.
295295
FlutterJNI flutterJNI = mock(FlutterJNI.class);
296296
when(flutterJNI.isAttached()).thenReturn(true);
@@ -301,6 +301,8 @@ public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() {
301301

302302
FlutterFragment fragment =
303303
FlutterFragment.withCachedEngine("my_cached_engine")
304+
// This enables the use of onBackPressedCallback, which is what
305+
// sends backs to the framework if setFrameworkHandlesBack is true.
304306
.shouldAutomaticallyHandleOnBackPressed(true)
305307
.build();
306308
FragmentActivity activity = getMockFragmentActivity();
@@ -318,8 +320,15 @@ public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() {
318320
TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate);
319321
fragment.setDelegateFactory(delegateFactory);
320322

323+
// Calling onBackPressed now will still be handled by Android (the default),
324+
// until setFrameworkHandlesBack is set to true.
321325
activity.getOnBackPressedDispatcher().onBackPressed();
326+
verify(mockDelegate, times(0)).onBackPressed();
322327

328+
// Setting setFrameworkHandlesBack to true means the delegate will receive
329+
// the back and Android won't handle it.
330+
fragment.setFrameworkHandlesBack(true);
331+
activity.getOnBackPressedDispatcher().onBackPressed();
323332
verify(mockDelegate, times(1)).onBackPressed();
324333
}
325334

@@ -361,10 +370,20 @@ public void handleOnBackPressed() {
361370
TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate);
362371
fragment.setDelegateFactory(delegateFactory);
363372

373+
assertTrue(callback.isEnabled());
374+
364375
assertTrue(fragment.popSystemNavigator());
365376

366377
verify(mockDelegate, never()).onBackPressed();
367378
assertTrue(onBackPressedCalled.get());
379+
assertTrue(callback.isEnabled());
380+
381+
callback.setEnabled(false);
382+
assertFalse(callback.isEnabled());
383+
assertTrue(fragment.popSystemNavigator());
384+
385+
verify(mockDelegate, never()).onBackPressed();
386+
assertFalse(callback.isEnabled());
368387
}
369388

370389
@Test

0 commit comments

Comments
 (0)