diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 636d287928d58..0fdca85bb1df8 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -840,6 +840,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroupCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index fa1b3fb948a80..75c914b0a8bc8 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -187,6 +187,7 @@ android_java_sources = [ "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java", "io/flutter/embedding/engine/FlutterEngineGroup.java", + "io/flutter/embedding/engine/FlutterEngineGroupCache.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterOverlaySurface.java", "io/flutter/embedding/engine/FlutterShellArgs.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 8ace803e5db0f..8ec8b419c2ba8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -9,7 +9,9 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; @@ -269,7 +271,7 @@ public NewEngineIntentBuilder(@NonNull Class extends FlutterActivity> activity } /** - * The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to + * The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to * "/". * * @param initialRoute The route. @@ -421,6 +423,151 @@ public Intent build(@NonNull Context context) { } } + /** + * Creates a {@link NewEngineInGroupIntentBuilder}, which can be used to configure an {@link + * Intent} to launch a {@code FlutterActivity} by internally creating a FlutterEngine from an + * existing {@link io.flutter.embedding.engine.FlutterEngineGroup} cached in a specified {@link + * io.flutter.embedding.engine.FlutterEngineGroupCache}, and creates a new {@link + * io.flutter.embedding.engine.FlutterEngine} by FlutterEngineGroup#createAndRunEngine + * + *
{@code + * // Create a FlutterEngineGroup, usually we could create it in onCreate method of Application. + * FlutterEngineGroup engineGroup = new FlutterEngineGroup(this); + * FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup); + * + * // use the intent that build by withNewEngineInGroup to start FlutterActivity + * Intent intent = FlutterActivity.withNewEngineInGroup("my_cached_engine_group_id") + * .dartEntrypoint("custom_entrypoint") + * .initialRoute("/custom/route") + * .backgroundMode(BackgroundMode.transparent) + * .build(context); + * startActivity(intent); + * }+ * + * @param engineGroupId A cached engine group ID. + * @return The builder. + */ + public static NewEngineInGroupIntentBuilder withNewEngineInGroup(@NonNull String engineGroupId) { + return new NewEngineInGroupIntentBuilder(FlutterActivity.class, engineGroupId); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new {@link + * FlutterEngine} by FlutterEngineGroup#createAndRunEngine. + */ + public static class NewEngineInGroupIntentBuilder { + private final Class extends FlutterActivity> activityClass; + private final String cachedEngineGroupId; + private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT; + private String initialRoute = DEFAULT_INITIAL_ROUTE; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code NewEngineInGroupIntentBuilder} to be used by subclasses + * of {@code FlutterActivity}. + * + *
Subclasses of {@code FlutterActivity} should provide their own static version of {@link + * #withNewEngineInGroup}, which returns an instance of {@code NewEngineInGroupIntentBuilder} + * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, e.g.: + * + *
{@code return new NewEngineInGroupIntentBuilder(MyFlutterActivity.class, + * cacheedEngineGroupId); } + * + *
{@code + * // Create a FlutterEngineGroup, usually we could create it in onCreate method of Application. + * FlutterEngineGroup engineGroup = new FlutterEngineGroup(this); + * FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup); + * + * // create NewEngineInGroupIntentBuilder, and start my custom FlutterActivity with the intent + * // that build by NewEngineInGroupIntentBuilder. + * FlutterActivity.NewEngineInGroupIntentBuilder intentBuilder = + * new FlutterActivity.NewEngineInGroupIntentBuilder( + * MyFlutterActivity.class, + * app.engineGroupId); + * intentBuilder.dartEntrypoint("main") + * .initialRoute("/custom/route") + * .backgroundMode(BackgroundMode.transparent); + * startActivity(intentBuilder.build(context)); + * }+ * + * @param activityClass A subclass of {@code FlutterActivity}. + * @param engineGroupId The engine group id. + */ + public NewEngineInGroupIntentBuilder( + @NonNull Class extends FlutterActivity> activityClass, @NonNull String engineGroupId) { + this.activityClass = activityClass; + this.cachedEngineGroupId = engineGroupId; + } + + /** + * The Dart entrypoint that will be executed in the newly created FlutterEngine as soon as the + * Dart snapshot is loaded. Default to "main". + * + * @param dartEntrypoint The dart entrypoint's name + * @return The engine group intent builder + */ + @NonNull + public NewEngineInGroupIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to + * "/". + * + * @param initialRoute The route. + * @return The engine group intent builder. + */ + @NonNull + public NewEngineInGroupIntentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * The mode of {@code FlutterActivity}'s background, either {@link BackgroundMode#opaque} or + * {@link BackgroundMode#transparent}. + * + *
The default background mode is {@link BackgroundMode#opaque}. + * + *
Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner + * {@link FlutterView} of this {@code FlutterActivity} to be configured with a {@link + * FlutterTextureView} to support transparency. This choice has a non-trivial performance + * impact. A transparent background should only be used if it is necessary for the app design + * being implemented. + * + *
A {@code FlutterActivity} that is configured with a background mode of {@link
+ * BackgroundMode#transparent} must have a theme applied to it that includes the following
+ * property: {@code
This preference can be controlled by setting a {@code This preference can be controlled with 2 methods:
+ *
+ * Subclasses may override this method to directly control the Dart entrypoint.
*/
@NonNull
public String getDartEntrypointFunctionName() {
+ if (getIntent().hasExtra(EXTRA_DART_ENTRYPOINT)) {
+ return getIntent().getStringExtra(EXTRA_DART_ENTRYPOINT);
+ }
+
try {
Bundle metaData = getMetaData();
String desiredDartEntrypoint =
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
index 79b2d69754d91..bcb23152d241a 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@@ -24,6 +24,8 @@
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
+import io.flutter.embedding.engine.FlutterEngineGroup;
+import io.flutter.embedding.engine.FlutterEngineGroupCache;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
@@ -213,6 +215,10 @@ void onAttach(@NonNull Context context) {
* Second, the {@code host} is given an opportunity to provide a {@link
* io.flutter.embedding.engine.FlutterEngine} via {@link Host#provideFlutterEngine(Context)}.
*
+ * Third, the {@code host} is given an FluttenEngineGroup Id, then used a cached {@link
+ * io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link FlutterEngine} by {@link
+ * FlutterEngineGroup#createAndRunEngine}
+ *
* If the {@code host} does not provide a {@link io.flutter.embedding.engine.FlutterEngine},
* then a new {@link FlutterEngine} is instantiated.
*/
@@ -241,6 +247,34 @@ void onAttach(@NonNull Context context) {
return;
}
+ // Third, check if the host wants to use a cached FlutterEngineGroup
+ // and create new FlutterEngine by FlutterEngineGroup#createAndRunEngine
+ String cachedEngineGroupId = host.getCachedEngineGroupId();
+ if (cachedEngineGroupId != null) {
+ FlutterEngineGroup flutterEngineGroup =
+ FlutterEngineGroupCache.getInstance().get(cachedEngineGroupId);
+ if (flutterEngineGroup == null) {
+ throw new IllegalStateException(
+ "The requested cached FlutterEngineGroup did not exist in the FlutterEngineGroupCache: '"
+ + cachedEngineGroupId
+ + "'");
+ }
+
+ String appBundlePathOverride = host.getAppBundlePath();
+ if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
+ appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath();
+ }
+
+ DartExecutor.DartEntrypoint dartEntrypoint =
+ new DartExecutor.DartEntrypoint(
+ appBundlePathOverride, host.getDartEntrypointFunctionName());
+ flutterEngine =
+ flutterEngineGroup.createAndRunEngine(
+ host.getContext(), dartEntrypoint, host.getInitialRoute());
+ isFlutterEngineFromHost = false;
+ return;
+ }
+
// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
@@ -876,6 +910,9 @@ private void ensureAlive() {
@Nullable
String getCachedEngineId();
+ @Nullable
+ String getCachedEngineGroupId();
+
/**
* Returns true if the {@link io.flutter.embedding.engine.FlutterEngine} used in this delegate
* should be destroyed when the host/delegate are destroyed.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
index a92e342333765..7106b4bc75c7c 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java
@@ -19,9 +19,11 @@ public class FlutterActivityLaunchConfigs {
/* package */ static final String HANDLE_DEEPLINKING_META_DATA_KEY =
"flutter_deeplinking_enabled";
// Intent extra arguments.
+ /* package */ static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint";
/* package */ static final String EXTRA_INITIAL_ROUTE = "route";
/* package */ static final String EXTRA_BACKGROUND_MODE = "background_mode";
/* package */ static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id";
+ /* package */ static final String EXTRA_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id";
/* package */ static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY =
"destroy_engine_with_activity";
/* package */ static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration";
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
index 365e6984f0b07..29dd1f0e511ec 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
@@ -136,6 +136,9 @@ public class FlutterFragment extends Fragment
* FlutterFragment}.
*/
protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id";
+
+ protected static final String ARG_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id";
+
/**
* True if the {@link io.flutter.embedding.engine.FlutterEngine} in the created {@code
* FlutterFragment} should be destroyed when the {@code FlutterFragment} is destroyed, false if
@@ -702,6 +705,263 @@ public An {@code IllegalStateException} will be thrown during the lifecycle of the {@code
+ * FlutterFragment} if a cached {@link io.flutter.embedding.engine.FlutterEngineGroup} is
+ * requested but does not exist in the {@link
+ * io.flutter.embedding.engine.FlutterEngineGroupCache}.
+ *
+ * To create a {@code FlutterFragment} that uses a new {@link
+ * io.flutter.embedding.engine.FlutterEngine} that created by {@link
+ * io.flutter.embedding.engine.FlutterEngineGroup#createAndRunEngine}
+ */
+ @NonNull
+ public static NewEngineInGroupFragmentBuilder withNewEngineInGroup(
+ @NonNull String engineGroupId) {
+ return new NewEngineInGroupFragmentBuilder(engineGroupId);
+ }
+
+ /**
+ * Builder that creates a new {@code FlutterFragment} that uses a cached {@link
+ * io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link
+ * io.flutter.embedding.engine.FlutterEngine} with {@code arguments} that correspond to the values
+ * set on this {@code Builder}.
+ *
+ * Subclasses of {@code FlutterFragment} that do not introduce any new arguments can use this
+ * {@code Builder} to construct instances of the subclass without subclassing this {@code
+ * Builder}. {@code MyFlutterFragment f = new
+ * FlutterFragment.NewEngineInGroupFragmentBuilder(MyFlutterFragment.class, engineGroupId)
+ * .someProperty(...) .someOtherProperty(...) .build Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
+ * {@code NewEngineInGroupFragmentBuilder} to add the new properties:
+ *
+ * See {@link TransparencyMode} for implications of this selection.
+ */
+ @NonNull
+ public NewEngineInGroupFragmentBuilder transparencyMode(
+ @NonNull TransparencyMode transparencyMode) {
+ this.transparencyMode = transparencyMode;
+ return this;
+ }
+
+ /**
+ * Whether or not this {@code FlutterFragment} should automatically attach its {@code Activity}
+ * as a control surface for its {@link io.flutter.embedding.engine.FlutterEngine}.
+ *
+ * Control surfaces are used to provide Android resources and lifecycle events to plugins
+ * that are attached to the {@link io.flutter.embedding.engine.FlutterEngine}. If {@code
+ * shouldAttachEngineToActivity} is true then this {@code FlutterFragment} will connect its
+ * {@link io.flutter.embedding.engine.FlutterEngine} to the surrounding {@code Activity}, along
+ * with any plugins that are registered with that {@link FlutterEngine}. This allows plugins to
+ * access the {@code Activity}, as well as receive {@code Activity}-specific calls, e.g., {@link
+ * android.app.Activity#onNewIntent(Intent)}. If {@code shouldAttachEngineToActivity} is false,
+ * then this {@code FlutterFragment} will not automatically manage the connection between its
+ * {@link io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. The
+ * {@code Activity} will need to be manually connected to this {@code FlutterFragment}'s {@link
+ * io.flutter.embedding.engine.FlutterEngine} by the app developer. See {@link
+ * FlutterEngine#getActivityControlSurface()}.
+ *
+ * One reason that a developer might choose to manually manage the relationship between the
+ * {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the developer
+ * wants to move the {@link FlutterEngine} somewhere else. For example, a developer might want
+ * the {@link io.flutter.embedding.engine.FlutterEngine} to outlive the surrounding {@code
+ * Activity} so that it can be used later in a different {@code Activity}. To accomplish this,
+ * the {@link io.flutter.embedding.engine.FlutterEngine} will need to be disconnected from the
+ * surrounding {@code Activity} at an unusual time, preventing this {@code FlutterFragment} from
+ * correctly managing the relationship between the {@link
+ * io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}.
+ *
+ * Another reason that a developer might choose to manually manage the relationship between
+ * the {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the
+ * developer wants to prevent, or explicitly control when the {@link
+ * io.flutter.embedding.engine.FlutterEngine}'s plugins have access to the surrounding {@code
+ * Activity}. For example, imagine that this {@code FlutterFragment} only takes up part of the
+ * screen and the app developer wants to ensure that none of the Flutter plugins are able to
+ * manipulate the surrounding {@code Activity}. In this case, the developer would not want the
+ * {@link io.flutter.embedding.engine.FlutterEngine} to have access to the {@code Activity},
+ * which can be accomplished by setting {@code shouldAttachEngineToActivity} to {@code false}.
+ */
+ @NonNull
+ public NewEngineInGroupFragmentBuilder shouldAttachEngineToActivity(
+ boolean shouldAttachEngineToActivity) {
+ this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
+ 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.
+ *
+ * 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 not invoke {@link #onBackPressed()}
+ * manually.
+ *
+ * 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 NewEngineInGroupFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
+ boolean shouldAutomaticallyHandleOnBackPressed) {
+ this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
+ return this;
+ }
+
+ /**
+ * Whether to delay the Android drawing pass till after the Flutter UI has been displayed.
+ *
+ * See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details.
+ */
+ @NonNull
+ public NewEngineInGroupFragmentBuilder shouldDelayFirstAndroidViewDraw(
+ @NonNull boolean shouldDelayFirstAndroidViewDraw) {
+ this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw;
+ return this;
+ }
+
+ /**
+ * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
+ *
+ * Subclasses should override this method to add new properties to the {@link Bundle}.
+ * Subclasses must call through to the super method to collect all existing property values.
+ */
+ @NonNull
+ protected Bundle createArgs() {
+ Bundle args = new Bundle();
+ args.putString(ARG_CACHED_ENGINE_GROUP_ID, cachedEngineGroupId);
+ args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint);
+ args.putString(ARG_INITIAL_ROUTE, initialRoute);
+ args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
+ args.putString(
+ ARG_FLUTTERVIEW_RENDER_MODE,
+ renderMode != null ? renderMode.name() : RenderMode.surface.name());
+ args.putString(
+ ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
+ 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);
+ args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw);
+ return args;
+ }
+
+ /**
+ * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on
+ * properties set on this {@code Builder}.
+ */
+ @NonNull
+ public Subclasses of {@code FlutterFragmentActivity} should provide their own static version of
+ * {@link #withNewEngineInGroup}, which returns an instance of {@code
+ * NewEngineInGroupIntentBuilder} constructed with a {@code Class} reference to the {@code
+ * FlutterFragmentActivity} subclass, e.g.:
+ *
+ * {@code return new NewEngineInGroupIntentBuilder(FlutterFragmentActivity.class,
+ * cacheedEngineGroupId); }
+ *
+ * @param activityClass A subclass of {@code FlutterFragmentActivity}.
+ * @param engineGroupId The engine group id.
+ */
+ public NewEngineInGroupIntentBuilder(
+ @NonNull Class extends FlutterFragmentActivity> activityClass,
+ @NonNull String engineGroupId) {
+ this.activityClass = activityClass;
+ this.cachedEngineGroupId = engineGroupId;
+ }
+
+ /**
+ * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded, default to
+ * "main".
+ *
+ * @param dartEntrypoint The dart entrypoint's name
+ * @return The engine group intent builder
+ */
+ @NonNull
+ public NewEngineInGroupIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
+ this.dartEntrypoint = dartEntrypoint;
+ return this;
+ }
+
+ /**
+ * The initial route that a Flutter app will render in this {@code FlutterFragmentActivity},
+ * defaults to "/".
+ */
+ @NonNull
+ public NewEngineInGroupIntentBuilder initialRoute(@NonNull String initialRoute) {
+ this.initialRoute = initialRoute;
+ return this;
+ }
+
+ /**
+ * The mode of {@code FlutterFragmentActivity}'s background, either {@link
+ * BackgroundMode#opaque} or {@link BackgroundMode#transparent}.
+ *
+ * The default background mode is {@link BackgroundMode#opaque}.
+ *
+ * Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner
+ * {@link FlutterView} of this {@code FlutterFragmentActivity} to be configured with a {@link
+ * FlutterTextureView} to support transparency. This choice has a non-trivial performance
+ * impact. A transparent background should only be used if it is necessary for the app design
+ * being implemented.
+ *
+ * A {@code FlutterFragmentActivity} that is configured with a background mode of {@link
+ * BackgroundMode#transparent} must have a theme applied to it that includes the following
+ * property: {@code The ID of a given {@link io.flutter.embedding.engine.FlutterEngineGroup} can be whatever
+ * {@code String} is desired.
+ *
+ * {@link io.flutter.embedding.android.FlutterActivity} and {@link
+ * io.flutter.embedding.android.FlutterFragment} use the {@code FlutterEngineGroupCache} singleton
+ * internally when instructed to use a cached {@link io.flutter.embedding.engine.FlutterEngineGroup}
+ * based on a given ID. See {@link
+ * io.flutter.embedding.android.FlutterActivity.NewEngineInGroupIntentBuilder} and {@link
+ * io.flutter.embedding.android.FlutterFragment#withNewEngineInGroup(String)} for related APIs.
+ */
+public class FlutterEngineGroupCache {
+ private static FlutterEngineGroupCache instance;
+
+ /**
+ * Returns the static singleton instance of {@code FlutterEngineGroupCache}.
+ *
+ * Creates a new instance if one does not yet exist.
+ */
+ @NonNull
+ public static FlutterEngineGroupCache getInstance() {
+ if (instance == null) {
+ instance = new FlutterEngineGroupCache();
+ }
+ return instance;
+ }
+
+ private final Map If a {@link io.flutter.embedding.engine.FlutterEngineGroup} is null, that {@link
+ * io.flutter.embedding.engine.FlutterEngineGroup} is removed from this cache.
+ */
+ public void put(@NonNull String engineGroupId, @Nullable FlutterEngineGroup engineGroup) {
+ if (engineGroup != null) {
+ cachedEngineGroups.put(engineGroupId, engineGroup);
+ } else {
+ cachedEngineGroups.remove(engineGroupId);
+ }
+ }
+
+ /**
+ * Removes any {@link io.flutter.embedding.engine.FlutterEngineGroup} that is currently in the
+ * cache that is identified by the given {@code engineGroupId}.
+ */
+ public void remove(@NonNull String engineGroupId) {
+ put(engineGroupId, null);
+ }
+
+ /**
+ * Removes all {@link io.flutter.embedding.engine.FlutterEngineGroup}'s that are currently in the
+ * cache.
+ */
+ public void clear() {
+ cachedEngineGroups.clear();
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index a50de5a38ba2d..c5cec2d48135f 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -27,6 +27,8 @@
import io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.Host;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
+import io.flutter.embedding.engine.FlutterEngineGroup;
+import io.flutter.embedding.engine.FlutterEngineGroupCache;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.loader.FlutterLoader;
@@ -205,6 +207,56 @@ public void itThrowsExceptionIfCachedEngineDoesNotExist() {
// Expect IllegalStateException.
}
+ @Test
+ public void itUsesNewEngineInGroupWhenProvided() {
+ // ---- Test setup ----
+ FlutterLoader mockFlutterLoader = mock(FlutterLoader.class);
+ when(mockFlutterLoader.findAppBundlePath()).thenReturn("default_flutter_assets/path");
+ FlutterInjector.setInstance(
+ new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build());
+ FlutterEngineGroup flutterEngineGroup = mock(FlutterEngineGroup.class);
+ FlutterEngineGroupCache.getInstance().put("my_flutter_engine_group", flutterEngineGroup);
+
+ // Adjust fake host to request cached engine group.
+ when(mockHost.getCachedEngineGroupId()).thenReturn("my_flutter_engine_group");
+ when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(null);
+ when(mockHost.shouldAttachEngineToActivity()).thenReturn(false);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // The FlutterEngine is obtained in onAttach().
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // If the engine in FlutterEngineGroup is being used, it should have sent a resumed lifecycle
+ // event.
+ DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint("/fake/path", "main");
+ verify(flutterEngineGroup, times(1))
+ .createAndRunEngine(mockHost.getContext(), entrypoint, mockHost.getInitialRoute());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void itThrowsExceptionIfNewEngineInGroupNotExist() {
+ // ---- Test setup ----
+ FlutterEngineGroupCache.getInstance().clear();
+
+ // Adjust fake host to request cached engine group that does not exist.
+ when(mockHost.getCachedEngineGroupId()).thenReturn("my_flutter_engine_group");
+ when(mockHost.getCachedEngineId()).thenReturn(null);
+ when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(null);
+ when(mockHost.shouldAttachEngineToActivity()).thenReturn(false);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // The FlutterEngine existence is verified in onAttach()
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // Expect IllegalStateException.
+ }
+
@Test
public void itGivesHostAnOpportunityToConfigureFlutterEngine() {
// ---- Test setup ----
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
index c0fccca8ddae7..8aeaeb99c5bf0 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
@@ -153,6 +153,31 @@ public void itCreatesNewEngineIntentWithRequestedSettings() {
assertEquals(TransparencyMode.transparent, flutterActivity.getTransparencyMode());
}
+ @Test
+ public void itCreatesNewEngineInGroupIntentWithRequestedSettings() {
+ Intent intent =
+ FlutterActivity.withNewEngineInGroup("my_cached_engine_group")
+ .dartEntrypoint("custom_entrypoint")
+ .initialRoute("/custom/route")
+ .backgroundMode(BackgroundMode.transparent)
+ .build(RuntimeEnvironment.application);
+ ActivityController
+ *
+ *
+ * If both preferences are set, the {@code Intent} preference takes priority.
*
*
+ *
+ *
+ * Once a {@code NewEngineInGroupFragmentBuilder} subclass is defined, the {@code FlutterFragment}
+ * subclass can be instantiated as follows. {@code MyFlutterFragment f = new MyBuilder()
+ * .someExistingProperty(...) .someNewProperty(...) .build