mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add automatic onBackPressed behavior to FlutterFragment (flutter/engine#25453)
This commit is contained in:
parent
575d32df63
commit
ce085d0129
@ -12,6 +12,7 @@ import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
@ -38,8 +39,11 @@ import io.flutter.plugin.platform.PlatformPlugin;
|
||||
* <li>{@link #onUserLeaveHint()}
|
||||
* </ol>
|
||||
*
|
||||
* Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be sure
|
||||
* to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than {@link
|
||||
* {@link #onBackPressed()} does not need to be called through if the fragment is constructed by one
|
||||
* of the builders with {@code shouldAutomaticallyHandleOnBackPressed(true)}.
|
||||
*
|
||||
* <p>Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be
|
||||
* sure to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than {@link
|
||||
* android.app.Activity#startActivityForResult(Intent, int)}. If the {@code Activity} version of the
|
||||
* method is invoked then this {@code Fragment} will never receive its {@link
|
||||
* Fragment#onActivityResult(int, int, Intent)} callback.
|
||||
@ -120,6 +124,12 @@ public class FlutterFragment extends Fragment
|
||||
* when this fragment is created and destroyed.
|
||||
*/
|
||||
protected static final String ARG_ENABLE_STATE_RESTORATION = "enable_state_restoration";
|
||||
/**
|
||||
* True if the fragment should receive {@link #onBackPressed()} events automatically, without
|
||||
* requiring an explicit activity call through.
|
||||
*/
|
||||
protected static final String ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED =
|
||||
"should_automatically_handle_on_back_pressed";
|
||||
|
||||
/**
|
||||
* Creates a {@code FlutterFragment} with a default configuration.
|
||||
@ -194,6 +204,7 @@ public class FlutterFragment extends Fragment
|
||||
private RenderMode renderMode = RenderMode.surface;
|
||||
private TransparencyMode transparencyMode = TransparencyMode.transparent;
|
||||
private boolean shouldAttachEngineToActivity = true;
|
||||
private boolean shouldAutomaticallyHandleOnBackPressed = false;
|
||||
|
||||
/**
|
||||
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
|
||||
@ -320,6 +331,28 @@ public class FlutterFragment extends Fragment
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this {@code FlutterFragment} should automatically receive {@link
|
||||
* #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled
|
||||
* by default.
|
||||
*
|
||||
* <p>When enabled, the activity will automatically dispatch back-press events to the fragment's
|
||||
* {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link
|
||||
* #onBackPressed()} in client code. If enabled, do <b>not</b> invoke {@link #onBackPressed()}
|
||||
* manually.
|
||||
*
|
||||
* <p>This behavior relies on the implementation of {@link #popSystemNavigator()}. It's not
|
||||
* recommended to override that method when enabling this attribute, but if you do, you should
|
||||
* always fall back to calling {@code super.popSystemNavigator()} when not relying on custom
|
||||
* behavior.
|
||||
*/
|
||||
@NonNull
|
||||
public NewEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
|
||||
boolean shouldAutomaticallyHandleOnBackPressed) {
|
||||
this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
|
||||
*
|
||||
@ -346,6 +379,8 @@ public class FlutterFragment extends Fragment
|
||||
transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name());
|
||||
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
|
||||
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
|
||||
args.putBoolean(
|
||||
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -428,6 +463,7 @@ public class FlutterFragment extends Fragment
|
||||
private RenderMode renderMode = RenderMode.surface;
|
||||
private TransparencyMode transparencyMode = TransparencyMode.transparent;
|
||||
private boolean shouldAttachEngineToActivity = true;
|
||||
private boolean shouldAutomaticallyHandleOnBackPressed = false;
|
||||
|
||||
private CachedEngineFragmentBuilder(@NonNull String engineId) {
|
||||
this(FlutterFragment.class, engineId);
|
||||
@ -527,6 +563,28 @@ public class FlutterFragment extends Fragment
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this {@code FlutterFragment} should automatically receive {@link
|
||||
* #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled
|
||||
* by default.
|
||||
*
|
||||
* <p>When enabled, the activity will automatically dispatch back-press events to the fragment's
|
||||
* {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link
|
||||
* #onBackPressed()} in client code. If enabled, do <b>not</b> invoke {@link #onBackPressed()}
|
||||
* manually.
|
||||
*
|
||||
* <p>Enabling this behavior relies on explicit behavior in {@link #popSystemNavigator()}. It's
|
||||
* not recommended to override that method when enabling this attribute, but if you do, you
|
||||
* should always fall back to calling {@code super.popSystemNavigator()} when not relying on
|
||||
* custom behavior.
|
||||
*/
|
||||
@NonNull
|
||||
public CachedEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
|
||||
boolean shouldAutomaticallyHandleOnBackPressed) {
|
||||
this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
|
||||
*
|
||||
@ -546,6 +604,8 @@ public class FlutterFragment extends Fragment
|
||||
ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
|
||||
transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name());
|
||||
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
|
||||
args.putBoolean(
|
||||
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -581,6 +641,14 @@ public class FlutterFragment extends Fragment
|
||||
// implementation for details about why it exists.
|
||||
@VisibleForTesting /* package */ FlutterActivityAndFragmentDelegate delegate;
|
||||
|
||||
private final OnBackPressedCallback onBackPressedCallback =
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onBackPressed();
|
||||
}
|
||||
};
|
||||
|
||||
public FlutterFragment() {
|
||||
// Ensure that we at least have an empty Bundle of arguments so that we don't
|
||||
// need to continually check for null arguments before grabbing one.
|
||||
@ -607,6 +675,9 @@ public class FlutterFragment extends Fragment
|
||||
super.onAttach(context);
|
||||
delegate = new FlutterActivityAndFragmentDelegate(this);
|
||||
delegate.onAttach(context);
|
||||
if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -744,6 +815,9 @@ public class FlutterFragment extends Fragment
|
||||
/**
|
||||
* The hardware back button was pressed.
|
||||
*
|
||||
* <p>If the fragment uses {@code shouldAutomaticallyHandleOnBackPressed(true)}, this method
|
||||
* should not be called through. It will be called automatically instead.
|
||||
*
|
||||
* <p>See {@link android.app.Activity#onBackPressed()}
|
||||
*/
|
||||
@ActivityCallThrough
|
||||
@ -1117,8 +1191,28 @@ public class FlutterFragment extends Fragment
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>Avoid overriding this method when using {@code
|
||||
* shouldAutomaticallyHandleOnBackPressed(true)}. If you do, you must always {@code return
|
||||
* super.popSystemNavigator()} rather than {@code return false}. Otherwise the navigation behavior
|
||||
* will recurse infinitely between this method and {@link #onBackPressed()}, breaking navigation.
|
||||
*/
|
||||
@Override
|
||||
public boolean popSystemNavigator() {
|
||||
if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity != null) {
|
||||
// Unless we disable the callback, the dispatcher call will trigger it. This will then
|
||||
// trigger the fragment's onBackPressed() implementation, which will call through to the
|
||||
// dart side and likely call back through to this method, creating an infinite call loop.
|
||||
onBackPressedCallback.setEnabled(false);
|
||||
activity.getOnBackPressedDispatcher().onBackPressed();
|
||||
onBackPressedCallback.setEnabled(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Hook for subclass. No-op if returns false.
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -9,10 +9,19 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.embedding.engine.FlutterEngineCache;
|
||||
import io.flutter.embedding.engine.FlutterJNI;
|
||||
import io.flutter.embedding.engine.loader.FlutterLoader;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@Config(manifest = Config.NONE)
|
||||
@ -114,4 +123,70 @@ public class FlutterFragmentTest {
|
||||
verify(mockDelegate, times(1)).onDestroyView();
|
||||
verify(mockDelegate, times(1)).onDetach();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itDelegatesOnBackPressedAutomaticallyWhenEnabled() {
|
||||
// We need to mock FlutterJNI to avoid triggering native code.
|
||||
FlutterJNI flutterJNI = mock(FlutterJNI.class);
|
||||
when(flutterJNI.isAttached()).thenReturn(true);
|
||||
|
||||
FlutterEngine flutterEngine =
|
||||
new FlutterEngine(
|
||||
RuntimeEnvironment.application, new FlutterLoader(), flutterJNI, null, false);
|
||||
FlutterEngineCache.getInstance().put("my_cached_engine", flutterEngine);
|
||||
|
||||
FlutterFragment fragment =
|
||||
FlutterFragment.withCachedEngine("my_cached_engine")
|
||||
.shouldAutomaticallyHandleOnBackPressed(true)
|
||||
.build();
|
||||
FragmentActivity activity = Robolectric.setupActivity(FragmentActivity.class);
|
||||
activity
|
||||
.getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(android.R.id.content, fragment)
|
||||
.commitNow();
|
||||
|
||||
FlutterActivityAndFragmentDelegate mockDelegate =
|
||||
mock(FlutterActivityAndFragmentDelegate.class);
|
||||
fragment.setDelegate(mockDelegate);
|
||||
|
||||
activity.onBackPressed();
|
||||
|
||||
verify(mockDelegate, times(1)).onBackPressed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itHandlesPopSystemNavigationAutomaticallyWhenEnabled() {
|
||||
// We need to mock FlutterJNI to avoid triggering native code.
|
||||
FlutterJNI flutterJNI = mock(FlutterJNI.class);
|
||||
when(flutterJNI.isAttached()).thenReturn(true);
|
||||
|
||||
FlutterEngine flutterEngine =
|
||||
new FlutterEngine(
|
||||
RuntimeEnvironment.application, new FlutterLoader(), flutterJNI, null, false);
|
||||
FlutterEngineCache.getInstance().put("my_cached_engine", flutterEngine);
|
||||
|
||||
FlutterFragment fragment =
|
||||
FlutterFragment.withCachedEngine("my_cached_engine")
|
||||
.shouldAutomaticallyHandleOnBackPressed(true)
|
||||
.build();
|
||||
FragmentActivity activity = Robolectric.setupActivity(FragmentActivity.class);
|
||||
activity
|
||||
.getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(android.R.id.content, fragment)
|
||||
.commitNow();
|
||||
OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
|
||||
when(callback.isEnabled()).thenReturn(true);
|
||||
activity.getOnBackPressedDispatcher().addCallback(callback);
|
||||
|
||||
FlutterActivityAndFragmentDelegate mockDelegate =
|
||||
mock(FlutterActivityAndFragmentDelegate.class);
|
||||
fragment.setDelegate(mockDelegate);
|
||||
|
||||
assertTrue(fragment.popSystemNavigator());
|
||||
|
||||
verify(mockDelegate, never()).onBackPressed();
|
||||
verify(callback, times(1)).handleOnBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user