diff --git a/DEPS b/DEPS index b742d90c4356a..35fd3fca0c60e 100644 --- a/DEPS +++ b/DEPS @@ -481,7 +481,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/robolectric_bundle', - 'version': 'last_updated:2019-07-29T15:27:42-0700' + 'version': 'last_updated:2019-08-02T16:01:27-0700' } ], 'condition': 'download_android_deps', diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 59dffca2ddf4b..f55a8eaf27f5d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -563,6 +563,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Splas FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.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 5c151e05db8cc..3aae6e3d2d5eb 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -139,6 +139,7 @@ action("flutter_shell_java") { "io/flutter/embedding/android/SplashScreenProvider.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java", + "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEnginePluginRegistry.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterShellArgs.java", @@ -330,6 +331,9 @@ action("robolectric_tests") { "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", + "test/io/flutter/embedding/android/FlutterActivityTest.java", + "test/io/flutter/embedding/android/FlutterFragmentTest.java", + "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", "test/io/flutter/util/PreconditionsTest.java", ] @@ -351,6 +355,7 @@ action("robolectric_tests") { "//third_party/robolectric/lib/common-1.1.1.jar", "//third_party/robolectric/lib/common-java8-1.1.1.jar", "//third_party/robolectric/lib/support-annotations-28.0.0.jar", + "//third_party/robolectric/lib/support-fragment-25.2.0.jar", "//third_party/robolectric/lib/mockito-all-1.10.19.jar", ] diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 2fa4488b9f458..390124afb2e2a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -47,11 +47,11 @@ * route may be specified explicitly by passing the name of the route as a {@code String} in * {@link #EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link". *
- * The Dart entrypoint and initial route can each be controlled using a {@link IntentBuilder} + * The Dart entrypoint and initial route can each be controlled using a {@link NewEngineIntentBuilder} * via the following methods: *
* The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of @@ -61,6 +61,37 @@ *
+ * {@code FlutterActivity} can be used with a cached {@link FlutterEngine} instead of creating a new + * one. Use {@link #withCachedEngine(String)} to build a {@code FlutterActivity} {@code Intent} that + * is configured to use an existing, cached {@link FlutterEngine}. {@link FlutterEngineCache} is the + * cache that is used to obtain a given cached {@link FlutterEngine}. An + * {@code IllegalStateException} will be thrown if a cached engine is requested but does not exist + * in the cache. + *
+ * It is generally recommended to use a cached {@link FlutterEngine} to avoid a momentary delay + * when initializing a new {@link FlutterEngine}. The two exceptions to using a cached + * {@link FlutterEngine} are: + *
+ *
+ * The following illustrates how to pre-warm and cache a {@link FlutterEngine}: + *
+ * {@code + * // Create and pre-warm a FlutterEngine. + * FlutterEngine flutterEngine = new FlutterEngine(context); + * flutterEngine + * .getDartExecutor() + * .executeDartEntrypoint(DartEntrypoint.createDefault()); + * + * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache. + * FlutterEngineCache.getInstance().put("my_engine", flutterEngine); + * } + *
* If Flutter is needed in a location that cannot use an {@code Activity}, consider using * a {@link FlutterFragment}. Using a {@link FlutterFragment} requires forwarding some calls from * an {@code Activity} to the {@link FlutterFragment}. @@ -149,6 +180,8 @@ public class FlutterActivity extends Activity protected static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint"; protected static final String EXTRA_INITIAL_ROUTE = "initial_route"; protected static final String EXTRA_BACKGROUND_MODE = "background_mode"; + protected static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + protected static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; // Default configuration. protected static final String DEFAULT_DART_ENTRYPOINT = "main"; @@ -161,42 +194,43 @@ public class FlutterActivity extends Activity */ @NonNull public static Intent createDefaultIntent(@NonNull Context launchContext) { - return createBuilder().build(launchContext); + return withNewEngine().build(launchContext); } /** - * Creates an {@link IntentBuilder}, which can be used to configure an {@link Intent} to - * launch a {@code FlutterActivity}. + * Creates an {@link NewEngineIntentBuilder}, which can be used to configure an {@link Intent} to + * launch a {@code FlutterActivity} that internally creates a new {@link FlutterEngine} using + * the desired Dart entrypoint, initial route, etc. */ @NonNull - public static IntentBuilder createBuilder() { - return new IntentBuilder(FlutterActivity.class); + public static NewEngineIntentBuilder withNewEngine() { + return new NewEngineIntentBuilder(FlutterActivity.class); } /** - * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with the - * desired configuration. + * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new + * {@link FlutterEngine} and the desired configuration. */ - public static class IntentBuilder { + public static class NewEngineIntentBuilder { private final Class extends FlutterActivity> activityClass; private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT; private String initialRoute = DEFAULT_INITIAL_ROUTE; private String backgroundMode = DEFAULT_BACKGROUND_MODE; /** - * Constructor that allows this {@code IntentBuilder} to be used by subclasses of + * Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of * {@code FlutterActivity}. *
* Subclasses of {@code FlutterActivity} should provide their own static version of - * {@link #createBuilder()}, which returns an instance of {@code IntentBuilder} + * {@link #withNewEngine()}, which returns an instance of {@code NewEngineIntentBuilder} * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, * e.g.: *
* {@code
- * return new IntentBuilder(MyFlutterActivity.class);
+ * return new NewEngineIntentBuilder(MyFlutterActivity.class);
* }
*/
- protected IntentBuilder(@NonNull Class extends FlutterActivity> activityClass) {
+ protected NewEngineIntentBuilder(@NonNull Class extends FlutterActivity> activityClass) {
this.activityClass = activityClass;
}
@@ -204,7 +238,7 @@ protected IntentBuilder(@NonNull Class extends FlutterActivity> activityClass)
* The name of the initial Dart method to invoke, defaults to "main".
*/
@NonNull
- public IntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
+ public NewEngineIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
this.dartEntrypoint = dartEntrypoint;
return this;
}
@@ -214,7 +248,7 @@ public IntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
* defaults to "/".
*/
@NonNull
- public IntentBuilder initialRoute(@NonNull String initialRoute) {
+ public NewEngineIntentBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
@@ -236,7 +270,7 @@ public IntentBuilder initialRoute(@NonNull String initialRoute) {
* following property: {@code
+ * Subclasses of {@code FlutterActivity} should provide their own static version of + * {@link #withNewEngine()}, which returns an instance of {@code CachedEngineIntentBuilder} + * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, + * e.g.: + *
+ * {@code + * return new CachedEngineIntentBuilder(MyFlutterActivity.class, engineId); + * } + */ + protected CachedEngineIntentBuilder( + @NonNull Class extends FlutterActivity> activityClass, + @NonNull String engineId + ) { + this.activityClass = activityClass; + this.cachedEngineId = engineId; + } + + /** + * Returns true if the cached {@link FlutterEngine} should be destroyed and removed from the + * cache when this {@code FlutterActivity} is destroyed. + *
+ * The default value is {@code false}. + */ + public CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) { + this.destroyEngineWithActivity = destroyEngineWithActivity; + 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
+ * The default value is {@code true} in cases where {@code FlutterActivity} created its own + * {@link FlutterEngine}, and {@code false} in cases where a cached {@link FlutterEngine} was + * provided. + */ + @Override + public boolean shouldDestroyEngineWithHost() { + return getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false); + } + /** * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. *
@@ -617,7 +763,7 @@ public String getAppBundlePath() { // Return the default app bundle path. // TODO(mattcarroll): move app bundle resolution into an appropriately named class. - return FlutterMain.findAppBundlePath(getApplicationContext()); + return FlutterMain.findAppBundlePath(); } /** @@ -752,16 +898,6 @@ public boolean shouldAttachEngineToActivity() { return true; } - /** - * Returns true if the {@link FlutterEngine} backing this {@code FlutterActivity} should - * outlive this {@code FlutterActivity}, or be destroyed when the {@code FlutterActivity} - * is destroyed. - */ - @Override - public boolean retainFlutterEngineAfterHostDestruction() { - return false; - } - @Override public void onFirstFrameRendered() {} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 37db2b1a6ce29..863925e6ae9ca 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -22,6 +22,7 @@ import io.flutter.Log; import io.flutter.app.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; @@ -182,7 +183,11 @@ private void initializeFlutter(@NonNull Context context) { /** * Obtains a reference to a FlutterEngine to back this delegate and its {@code host}. *
- * First, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via + *
+ * First, the {@code host} is asked if it would like to use a cached {@link FlutterEngine}, and + * if so, the cached {@link FlutterEngine} is retrieved. + *
+ * Second, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via * {@link Host#provideFlutterEngine(Context)}. *
* If the {@code host} does not provide a {@link FlutterEngine}, then a new {@link FlutterEngine} @@ -191,9 +196,21 @@ private void initializeFlutter(@NonNull Context context) { private void setupFlutterEngine() { Log.d(TAG, "Setting up FlutterEngine."); - // First, defer to subclasses for a custom FlutterEngine. + // First, check if the host wants to use a cached FlutterEngine. + String cachedEngineId = host.getCachedEngineId(); + if (cachedEngineId != null) { + flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId); + isFlutterEngineFromHost = true; + if (flutterEngine == null) { + throw new IllegalStateException("The requested cached FlutterEngine did not exist in the FlutterEngineCache: '" + cachedEngineId + "'"); + } + return; + } + + // Second, defer to subclasses for a custom FlutterEngine. flutterEngine = host.provideFlutterEngine(host.getContext()); if (flutterEngine != null) { + isFlutterEngineFromHost = true; return; } @@ -275,6 +292,11 @@ public void run() { * {@code flutterEngine} must be non-null when invoking this method. */ private void doInitialFlutterViewRun() { + // Don't attempt to start a FlutterEngine if we're using a cached FlutterEngine. + if (host.getCachedEngineId() != null) { + return; + } + if (flutterEngine.getDartExecutor().isExecutingDart()) { // No warning is logged because this situation will happen on every config // change if the developer does not choose to retain the Fragment instance. @@ -387,7 +409,7 @@ void onDestroyView() { * if it was previously attached. *
+ * The default value is {@code true} in cases where {@code FlutterFragment} created its own + * {@link FlutterEngine}, and {@code false} in cases where a cached {@link FlutterEngine} was + * provided. + */ + boolean shouldDestroyEngineWithHost(); + /** * Returns the Dart entrypoint that should run when a new {@link FlutterEngine} is * created. @@ -649,15 +694,6 @@ private void ensureAlive() { */ boolean shouldAttachEngineToActivity(); - /** - * Returns true if the {@link FlutterEngine} used in this delegate should outlive the - * delegate. - *
- * If {@code false} is returned, the {@link FlutterEngine} used in this delegate will be - * destroyed when the delegate is destroyed. - */ - boolean retainFlutterEngineAfterHostDestruction(); - /** * Invoked by this delegate when its {@link FlutterView} has rendered its first Flutter * frame. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index c004d7c8c4a6e..6b42858184e85 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -47,6 +47,34 @@ * If convenient, consider using a {@link FlutterActivity} instead of a {@code FlutterFragment} to * avoid the work of forwarding calls. *
+ * {@code FlutterFragment} supports the use of an existing, cached {@link FlutterEngine}. To use a + * cached {@link FlutterEngine}, ensure that the {@link FlutterEngine} is stored in + * {@link FlutterEngineCache} and then use {@link #withCachedEngine(String)} to build a + * {@code FlutterFragment} with the cached {@link FlutterEngine}'s ID. + *
+ * It is generally recommended to use a cached {@link FlutterEngine} to avoid a momentary delay + * when initializing a new {@link FlutterEngine}. The two exceptions to using a cached + * {@link FlutterEngine} are: + *
+ *
+ * The following illustrates how to pre-warm and cache a {@link FlutterEngine}: + *
+ * {@code + * // Create and pre-warm a FlutterEngine. + * FlutterEngine flutterEngine = new FlutterEngine(context); + * flutterEngine + * .getDartExecutor() + * .executeDartEntrypoint(DartEntrypoint.createDefault()); + * + * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache. + * FlutterEngineCache.getInstance().put("my_engine", flutterEngine); + * } + *
* If Flutter is needed in a location that can only use a {@code View}, consider using a * {@link FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a @@ -85,45 +113,83 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm * See {@link #shouldAttachEngineToActivity()}. */ protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity"; + /** + * The ID of a {@link FlutterEngine} cached in {@link FlutterEngineCache} that will be used within + * the created {@code FlutterFragment}. + */ + protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id"; + /** + * True if the {@link FlutterEngine} in the created {@code FlutterFragment} should be destroyed + * when the {@code FlutterFragment} is destroyed, false if the {@link FlutterEngine} should + * outlive the {@code FlutterFragment}. + */ + protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment"; + + /** + * Creates a {@code FlutterFragment} with a default configuration. + *
+ * {@code FlutterFragment}'s default configuration creates a new {@link FlutterEngine} within + * the {@code FlutterFragment} and uses the following settings: + *
+ * To use a new {@link FlutterEngine} with different settings, use {@link #withNewEngine()}. + *
+ * To use a cached {@link FlutterEngine} instead of creating a new one, use + * {@link #withCachedEngine(String)}. + */ + @NonNull + public static FlutterFragment createDefault() { + return new NewEngineFragmentBuilder().build(); + } + /** + * Returns a {@link NewEngineFragmentBuilder} to create a {@code FlutterFragment} with a new + * {@link FlutterEngine} and a desired engine configuration. + */ @NonNull - public static FlutterFragment createDefaultFlutterFragment() { - return new FlutterFragment.Builder().build(); + public static NewEngineFragmentBuilder withNewEngine() { + return new NewEngineFragmentBuilder(); } /** * Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond - * to the values set on this {@code Builder}. + * to the values set on this {@code NewEngineFragmentBuilder}. *
* To create a {@code FlutterFragment} with default {@code arguments}, invoke - * {@link #createDefaultFlutterFragment()}. + * {@link #createDefault()}. *
* 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 NewEngineFragmentBuilder} to construct instances of the subclass without subclassing
+ * this {@code NewEngineFragmentBuilder}.
* {@code
- * MyFlutterFragment f = new FlutterFragment.Builder(MyFlutterFragment.class)
+ * MyFlutterFragment f = new FlutterFragment.NewEngineFragmentBuilder(MyFlutterFragment.class)
* .someProperty(...)
* .someOtherProperty(...)
* .build
* Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
- * {@code Builder} to add the new properties:
+ * {@code NewEngineFragmentBuilder} to add the new properties:
*
+ * An {@code IllegalStateException} will be thrown during the lifecycle of the
+ * {@code FlutterFragment} if a cached {@link FlutterEngine} is requested but does not exist in
+ * the cache.
+ *
+ * To create a {@code FlutterFragment} that uses a new {@link FlutterEngine}, use
+ * {@link #createDefault()} or {@link #withNewEngine()}.
+ */
+ @NonNull
+ public static CachedEngineFragmentBuilder withCachedEngine(@NonNull String engineId) {
+ return new CachedEngineFragmentBuilder(engineId);
+ }
+
+ /**
+ * Builder that creates a new {@code FlutterFragment} that uses a cached {@link 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.CachedEngineFragmentBuilder(MyFlutterFragment.class)
+ * .someProperty(...)
+ * .someOtherProperty(...)
+ * .build
+ * Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
+ * {@code CachedEngineFragmentBuilder} to add the new properties:
+ *
+ * See {@link FlutterView.TransparencyMode} for implications of this selection.
+ */
+ @NonNull
+ public CachedEngineFragmentBuilder transparencyMode(@NonNull FlutterView.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 FlutterEngine}.
+ *
+ * Control surfaces are used to provide Android resources and lifecycle events to
+ * plugins that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity}
+ * is true then this {@code FlutterFragment} will connect its {@link 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 FlutterEngine} and the surrounding
+ * {@code Activity}. The {@code Activity} will need to be manually connected to this
+ * {@code FlutterFragment}'s {@link 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 FlutterEngine} is if the developer wants to move the
+ * {@link FlutterEngine} somewhere else. For example, a developer might want the
+ * {@link FlutterEngine} to outlive the surrounding {@code Activity} so that it can be used
+ * later in a different {@code Activity}. To accomplish this, the {@link 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 FlutterEngine} and the surrounding {@code Activity}.
+ *
+ * Another reason that a developer might choose to manually manage the relationship between the
+ * {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or explicitly
+ * control when the {@link 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 FlutterEngine} to have access to the {@code Activity}, which can be accomplished by
+ * setting {@code shouldAttachEngineToActivity} to {@code false}.
+ */
+ @NonNull
+ public CachedEngineFragmentBuilder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) {
+ this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
+ 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_ID, engineId);
+ args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment);
+ args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name());
+ args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name());
+ args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
+ return args;
+ }
+
+ /**
+ * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on
+ * properties set on this {@code CachedEngineFragmentBuilder}.
+ */
+ @NonNull
+ public
+ * Defaults to true if no custom {@link FlutterEngine is provided}, false if a custom
+ * {@link FlutterEngine} is provided.
+ */
+ @Override
+ public boolean shouldDestroyEngineWithHost() {
+ return getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, false);
+ }
+
/**
* Returns the name of the Dart method that this {@code FlutterFragment} should execute to
* start a Flutter app.
@@ -662,32 +940,16 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
}
/**
- * See {@link Builder#shouldAttachEngineToActivity()}.
+ * See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and
+ * {@link CachedEngineFragmentBuilder#shouldAttachEngineToActivity()}.
*
- * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate}
*/
@Override
public boolean shouldAttachEngineToActivity() {
return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY);
}
- /**
- * Returns true if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive
- * the {@code FlutterFragment}, itself.
- *
- * Defaults to false. This method can be overridden in subclasses to retain the
- * {@link FlutterEngine}.
- *
- * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
- */
- // TODO(mattcarroll): consider a dynamic determination of this preference based on whether the
- // engine was created automatically, or if the engine was provided manually.
- // Manually provided engines should probably not be destroyed.
- @Override
- public boolean retainFlutterEngineAfterHostDestruction() {
- return false;
- }
-
/**
* Invoked after the {@link FlutterView} within this {@code FlutterFragment} renders its first
* frame.
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java
index 600d952cd4525..0a33aca1de717 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java
@@ -1,3 +1,7 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
package io.flutter.embedding.engine;
import android.arch.lifecycle.DefaultLifecycleObserver;
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java
new file mode 100644
index 0000000000000..99011fc17eb56
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java
@@ -0,0 +1,86 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.embedding.engine;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Static singleton cache that holds {@link FlutterEngine} instances identified by {@code String}s.
+ *
+ * The ID of a given {@link FlutterEngine} can be whatever {@code String} is desired.
+ *
+ * {@code FlutterEngineCache} is useful for storing pre-warmed {@link FlutterEngine} instances.
+ * {@link io.flutter.embedding.android.FlutterActivity} and
+ * {@link io.flutter.embedding.android.FlutterFragment} use the {@code FlutterEngineCache} singleton
+ * internally when instructed to use a cached {@link FlutterEngine} based on a given ID. See
+ * {@link io.flutter.embedding.android.FlutterActivity.CachedEngineIntentBuilder} and
+ * {@link io.flutter.embedding.android.FlutterFragment#withCachedEngine(String)} for related APIs.
+ */
+public class FlutterEngineCache {
+ private static FlutterEngineCache instance;
+
+ /**
+ * Returns the static singleton instance of {@code FlutterEngineCache}.
+ *
+ * Creates a new instance if one does not yet exist.
+ */
+ @NonNull
+ public static FlutterEngineCache getInstance() {
+ if (instance == null) {
+ instance = new FlutterEngineCache();
+ }
+ return instance;
+ }
+
+ private final Map
+ * If a {@link FlutterEngine} already exists in this cache for the given {@code engineId}, that
+ * {@link FlutterEngine} is removed from this cache.
+ */
+ public void put(@NonNull String engineId, @Nullable FlutterEngine engine) {
+ if (engine != null) {
+ cachedEngines.put(engineId, engine);
+ } else {
+ cachedEngines.remove(engineId);
+ }
+ }
+
+ /**
+ * Removes any {@link FlutterEngine} that is currently in the cache that is identified by
+ * the given {@code engineId}.
+ */
+ public void remove(@NonNull String engineId) {
+ put(engineId, null);
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
index 23b9a6010fffd..e6e8ee38e1d3b 100644
--- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java
+++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
@@ -4,19 +4,24 @@
package io.flutter;
-import io.flutter.SmokeTest;
-import io.flutter.util.PreconditionsTest;
-import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
-
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
+import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
+import io.flutter.embedding.android.FlutterActivityTest;
+import io.flutter.embedding.android.FlutterFragmentTest;
+import io.flutter.embedding.engine.FlutterEngineCacheTest;
+import io.flutter.util.PreconditionsTest;
+
@RunWith(Suite.class)
@SuiteClasses({
PreconditionsTest.class,
SmokeTest.class,
+ FlutterActivityTest.class,
+ FlutterFragmentTest.class,
FlutterActivityAndFragmentDelegateTest.class,
+ FlutterEngineCacheTest.class
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
public class FlutterTestSuite {}
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 9350e83789cf3..9adab51588954 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -5,7 +5,6 @@
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
@@ -17,6 +16,7 @@
import org.robolectric.annotation.Config;
import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface;
@@ -27,17 +27,16 @@
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
-import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.view.FlutterMain;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,8 +45,7 @@
@RunWith(RobolectricTestRunner.class)
public class FlutterActivityAndFragmentDelegateTest {
private FlutterEngine mockFlutterEngine;
- private FakeHost fakeHost;
- private FakeHost spyHost;
+ private FlutterActivityAndFragmentDelegate.Host mockHost;
@Before
public void setup() {
@@ -59,12 +57,20 @@ public void setup() {
// being tested.
mockFlutterEngine = mockFlutterEngine();
- // Create a fake Host, which is required by the delegate being tested.
- fakeHost = new FakeHost();
- fakeHost.flutterEngine = mockFlutterEngine;
-
- // Create a spy around the FakeHost so that we can verify method invocations.
- spyHost = spy(fakeHost);
+ // Create a mocked Host, which is required by the delegate being tested.
+ mockHost = mock(FlutterActivityAndFragmentDelegate.Host.class);
+ when(mockHost.getContext()).thenReturn(RuntimeEnvironment.application);
+ when(mockHost.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class));
+ when(mockHost.getLifecycle()).thenReturn(mock(Lifecycle.class));
+ when(mockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[]{}));
+ when(mockHost.getDartEntrypointFunctionName()).thenReturn("main");
+ when(mockHost.getAppBundlePath()).thenReturn("/fake/path");
+ when(mockHost.getInitialRoute()).thenReturn("/");
+ when(mockHost.getRenderMode()).thenReturn(FlutterView.RenderMode.surface);
+ when(mockHost.getTransparencyMode()).thenReturn(FlutterView.TransparencyMode.transparent);
+ when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(mockFlutterEngine);
+ when(mockHost.shouldAttachEngineToActivity()).thenReturn(true);
+ when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true);
}
@After
@@ -77,7 +83,7 @@ public void teardown() {
public void itSendsLifecycleEventsToFlutter() {
// ---- Test setup ----
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// We're testing lifecycle behaviors, which require/expect that certain methods have already
// been executed by the time they run. Therefore, we run those expected methods first.
@@ -117,41 +123,88 @@ public void itSendsLifecycleEventsToFlutter() {
public void itDefersToTheHostToProvideFlutterEngine() {
// ---- Test setup ----
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is created in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the host was asked to provide a FlutterEngine.
- verify(spyHost, times(1)).provideFlutterEngine(any(Context.class));
+ verify(mockHost, times(1)).provideFlutterEngine(any(Context.class));
// Verify that the delegate's FlutterEngine is our mock FlutterEngine.
assertEquals("The delegate failed to use the host's FlutterEngine.", mockFlutterEngine, delegate.getFlutterEngine());
}
+ @Test
+ public void itUsesCachedEngineWhenProvided() {
+ // ---- Test setup ----
+ // Place a FlutterEngine in the static cache.
+ FlutterEngine cachedEngine = mockFlutterEngine();
+ FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
+
+ // Adjust fake host to request cached engine.
+ when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine");
+
+ // 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);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+
+ // --- Verify that the cached engine was used ---
+ // Verify that the non-cached engine was not used.
+ verify(mockFlutterEngine.getDartExecutor(), never()).executeDartEntrypoint(any(DartExecutor.DartEntrypoint.class));
+
+ // We should never instruct a cached engine to execute Dart code - it should already be executing it.
+ verify(cachedEngine.getDartExecutor(), never()).executeDartEntrypoint(any(DartExecutor.DartEntrypoint.class));
+
+ // If the cached engine is being used, it should have sent a resumed lifecycle event.
+ verify(cachedEngine.getLifecycleChannel(), times(1)).appIsResumed();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void itThrowsExceptionIfCachedEngineDoesNotExist() {
+ // ---- Test setup ----
+ // Adjust fake host to request cached engine that does not exist.
+ when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine");
+
+ // 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 ----
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is created in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
// Verify that the host was asked to configure our FlutterEngine.
- verify(spyHost, times(1)).configureFlutterEngine(mockFlutterEngine);
+ verify(mockHost, times(1)).configureFlutterEngine(mockFlutterEngine);
}
@Test
public void itSendsInitialRouteToFlutter() {
// ---- Test setup ----
// Set initial route on our fake Host.
- spyHost.initialRoute = "/my/route";
+ when(mockHost.getInitialRoute()).thenReturn("/my/route");
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The initial route is sent in onStart().
@@ -167,8 +220,8 @@ public void itSendsInitialRouteToFlutter() {
public void itExecutesDartEntrypointProvidedByHost() {
// ---- Test setup ----
// Set Dart entrypoint parameters on fake host.
- spyHost.appBundlePath = "/my/bundle/path";
- spyHost.dartEntrypointFunctionName = "myEntrypoint";
+ when(mockHost.getAppBundlePath()).thenReturn("/my/bundle/path");
+ when(mockHost.getDartEntrypointFunctionName()).thenReturn("myEntrypoint");
// Create the DartEntrypoint that we expect to be executed.
DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint(
@@ -177,7 +230,7 @@ public void itExecutesDartEntrypointProvidedByHost() {
);
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// Dart is executed in onStart().
@@ -196,10 +249,10 @@ public void itExecutesDartEntrypointProvidedByHost() {
public void itAttachesFlutterToTheActivityIfDesired() {
// ---- Test setup ----
// Declare that the host wants Flutter to attach to the surrounding Activity.
- spyHost.shouldAttachToActivity = true;
+ when(mockHost.shouldAttachEngineToActivity()).thenReturn(true);
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// Flutter is attached to the surrounding Activity in onAttach.
@@ -222,10 +275,10 @@ public void itAttachesFlutterToTheActivityIfDesired() {
public void itDoesNotAttachFlutterToTheActivityIfNotDesired() {
// ---- Test setup ----
// Declare that the host does NOT want Flutter to attach to the surrounding Activity.
- spyHost.shouldAttachToActivity = false;
+ when(mockHost.shouldAttachEngineToActivity()).thenReturn(false);
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// Flutter is attached to the surrounding Activity in onAttach.
@@ -244,7 +297,7 @@ public void itDoesNotAttachFlutterToTheActivityIfNotDesired() {
@Test
public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -260,7 +313,7 @@ public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() {
@Test
public void itForwardsOnRequestPermissionsResultToFlutterEngine() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -276,7 +329,7 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() {
@Test
public void itForwardsOnNewIntentToFlutterEngine() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -292,7 +345,7 @@ public void itForwardsOnNewIntentToFlutterEngine() {
@Test
public void itForwardsOnActivityResultToFlutterEngine() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -308,7 +361,7 @@ public void itForwardsOnActivityResultToFlutterEngine() {
@Test
public void itForwardsOnUserLeaveHintToFlutterEngine() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -324,7 +377,7 @@ public void itForwardsOnUserLeaveHintToFlutterEngine() {
@Test
public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -340,7 +393,7 @@ public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() {
@Test
public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() {
// Create the real object that we're testing.
- FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
// --- Execute the behavior under test ---
// The FlutterEngine is setup in onAttach().
@@ -353,6 +406,117 @@ public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() {
verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning();
}
+ @Test
+ public void itDestroysItsOwnEngineIfHostRequestsIt() {
+ // ---- Test setup ----
+ // Adjust fake host to request engine destruction.
+ when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // Push the delegate through all lifecycle methods all the way to destruction.
+ delegate.onAttach(RuntimeEnvironment.application);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+ delegate.onPause();
+ delegate.onStop();
+ delegate.onDestroyView();
+ delegate.onDetach();
+
+ // --- Verify that the cached engine was destroyed ---
+ verify(mockFlutterEngine, times(1)).destroy();
+ }
+
+ @Test
+ public void itDoesNotDestroyItsOwnEngineWhenHostSaysNotTo() {
+ // ---- Test setup ----
+ // Adjust fake host to request engine destruction.
+ when(mockHost.shouldDestroyEngineWithHost()).thenReturn(false);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // Push the delegate through all lifecycle methods all the way to destruction.
+ delegate.onAttach(RuntimeEnvironment.application);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+ delegate.onPause();
+ delegate.onStop();
+ delegate.onDestroyView();
+ delegate.onDetach();
+
+ // --- Verify that the cached engine was destroyed ---
+ verify(mockFlutterEngine, never()).destroy();
+ }
+
+ @Test
+ public void itDestroysCachedEngineWhenHostRequestsIt() {
+ // ---- Test setup ----
+ // Place a FlutterEngine in the static cache.
+ FlutterEngine cachedEngine = mockFlutterEngine();
+ FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
+
+ // Adjust fake host to request cached engine.
+ when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine");
+
+ // Adjust fake host to request engine destruction.
+ when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // Push the delegate through all lifecycle methods all the way to destruction.
+ delegate.onAttach(RuntimeEnvironment.application);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+ delegate.onPause();
+ delegate.onStop();
+ delegate.onDestroyView();
+ delegate.onDetach();
+
+ // --- Verify that the cached engine was destroyed ---
+ verify(cachedEngine, times(1)).destroy();
+ assertNull(FlutterEngineCache.getInstance().get("my_flutter_engine"));
+ }
+
+ @Test
+ public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() {
+ // ---- Test setup ----
+ // Place a FlutterEngine in the static cache.
+ FlutterEngine cachedEngine = mockFlutterEngine();
+ FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine);
+
+ // Adjust fake host to request cached engine.
+ when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine");
+
+ // Adjust fake host to request engine retention.
+ when(mockHost.shouldDestroyEngineWithHost()).thenReturn(false);
+
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // --- Execute the behavior under test ---
+ // Push the delegate through all lifecycle methods all the way to destruction.
+ delegate.onAttach(RuntimeEnvironment.application);
+ delegate.onCreateView(null, null, null);
+ delegate.onStart();
+ delegate.onResume();
+ delegate.onPause();
+ delegate.onStop();
+ delegate.onDestroyView();
+ delegate.onDetach();
+
+ // --- Verify that the cached engine was NOT destroyed ---
+ verify(cachedEngine, never()).destroy();
+ }
+
/**
* Creates a mock {@link FlutterEngine}.
*
@@ -387,115 +551,4 @@ private FlutterEngine mockFlutterEngine() {
return engine;
}
-
- /**
- * A {@link FlutterActivityAndFragmentDelegate.Host} that returns values desired by this
- * test suite.
- *
- * Sane defaults are set for all properties. Tests in this suite can alter {@code FakeHost}
- * properties as needed for each test.
- */
- private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host {
- private FlutterEngine flutterEngine;
- private String initialRoute = null;
- private String appBundlePath = "fake/path/";
- private String dartEntrypointFunctionName = "main";
- private Activity activity;
- private boolean shouldAttachToActivity = false;
- private boolean retainFlutterEngine = false;
-
- @NonNull
- @Override
- public Context getContext() {
- return RuntimeEnvironment.application;
- }
-
- @Nullable
- @Override
- public Activity getActivity() {
- if (activity == null) {
- // We must provide a real (or close to real) Activity because it is passed to
- // the FlutterView that the delegate instantiates.
- activity = Robolectric.setupActivity(Activity.class);
- }
-
- return activity;
- }
-
- @NonNull
- @Override
- public Lifecycle getLifecycle() {
- return mock(Lifecycle.class);
- }
-
- @NonNull
- @Override
- public FlutterShellArgs getFlutterShellArgs() {
- return new FlutterShellArgs(new String[]{});
- }
-
- @NonNull
- @Override
- public String getDartEntrypointFunctionName() {
- return dartEntrypointFunctionName;
- }
-
- @NonNull
- @Override
- public String getAppBundlePath() {
- return appBundlePath;
- }
-
- @Nullable
- @Override
- public String getInitialRoute() {
- return initialRoute;
- }
-
- @NonNull
- @Override
- public FlutterView.RenderMode getRenderMode() {
- return FlutterView.RenderMode.surface;
- }
-
- @NonNull
- @Override
- public FlutterView.TransparencyMode getTransparencyMode() {
- return FlutterView.TransparencyMode.opaque;
- }
-
- @Nullable
- @Override
- public SplashScreen provideSplashScreen() {
- return null;
- }
-
- @Nullable
- @Override
- public FlutterEngine provideFlutterEngine(@NonNull Context context) {
- return flutterEngine;
- }
-
- @Nullable
- @Override
- public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
- return null;
- }
-
- @Override
- public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {}
-
- @Override
- public boolean shouldAttachEngineToActivity() {
- return shouldAttachToActivity;
- }
-
- @Override
- public boolean retainFlutterEngineAfterHostDestruction() {
- return retainFlutterEngine;
- }
-
- @Override
- public void onFirstFrameRendered() {}
- }
}
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
new file mode 100644
index 0000000000000..694b349a8aa2f
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
@@ -0,0 +1,87 @@
+package io.flutter.embedding.android;
+
+import android.content.Intent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@Config(manifest=Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class FlutterActivityTest {
+ @Test
+ public void itCreatesDefaultIntentWithExpectedDefaults() {
+ Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application);
+ ActivityController
*
- * Once a {@code Builder} subclass is defined, the {@code FlutterFragment} subclass can be
- * instantiated as follows.
+ * Once a {@code NewEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment}
+ * subclass can be instantiated as follows.
* {@code
* MyFlutterFragment f = new MyBuilder()
* .someExistingProperty(...)
@@ -131,7 +197,7 @@ public static FlutterFragment createDefaultFlutterFragment() {
* .build
+ *
+ * Once a {@code CachedEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment}
+ * subclass can be instantiated as follows.
+ * {@code
+ * MyFlutterFragment f = new MyBuilder()
+ * .someExistingProperty(...)
+ * .someNewProperty(...)
+ * .build