forked from firka/flutter
Android embedding refactor pr40 add static engine cache (flutter/engine#10481)
This commit is contained in:
2
DEPS
2
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',
|
||||
|
||||
@@ -565,6 +565,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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ import io.flutter.view.FlutterMain;
|
||||
* 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".
|
||||
* <p>
|
||||
* 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:
|
||||
* <ul>
|
||||
* <li>{@link IntentBuilder#dartEntrypoint}</li>
|
||||
* <li>{@link IntentBuilder#initialRoute}</li>
|
||||
* <li>{@link NewEngineIntentBuilder#dartEntrypoint}</li>
|
||||
* <li>{@link NewEngineIntentBuilder#initialRoute}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of
|
||||
@@ -61,6 +61,37 @@ import io.flutter.view.FlutterMain;
|
||||
* <li>{@link #getDartEntrypointFunctionName()}</li>
|
||||
* <li>{@link #getInitialRoute()}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* 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:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>When {@code FlutterActivity} is the first {@code Activity} displayed by the app, because
|
||||
* pre-warming a {@link FlutterEngine} would have no impact in this situation.</li>
|
||||
* <li>When you are unsure when/if you will need to display a Flutter experience.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The following illustrates how to pre-warm and cache a {@link FlutterEngine}:
|
||||
* <p>
|
||||
* {@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);
|
||||
* }
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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.:
|
||||
* <p>
|
||||
* {@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 @@ public class FlutterActivity extends Activity
|
||||
* 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 class FlutterActivity extends Activity
|
||||
* 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 class FlutterActivity extends Activity
|
||||
* following property: {@code <item name="android:windowIsTranslucent">true</item>}.
|
||||
*/
|
||||
@NonNull
|
||||
public IntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
|
||||
public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
|
||||
this.backgroundMode = backgroundMode.name();
|
||||
return this;
|
||||
}
|
||||
@@ -250,6 +284,93 @@ public class FlutterActivity extends Activity
|
||||
return new Intent(context, activityClass)
|
||||
.putExtra(EXTRA_DART_ENTRYPOINT, dartEntrypoint)
|
||||
.putExtra(EXTRA_INITIAL_ROUTE, initialRoute)
|
||||
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
|
||||
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CachedEngineIntentBuilder}, which can be used to configure an {@link Intent}
|
||||
* to launch a {@code FlutterActivity} that internally uses an existing {@link FlutterEngine} that
|
||||
* is cached in {@link FlutterEngineCache}.
|
||||
*/
|
||||
public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) {
|
||||
return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to create an {@code Intent} that launches a {@code FlutterActivity} with an existing
|
||||
* {@link FlutterEngine} that is cached in {@link FlutterEngineCache}.
|
||||
*/
|
||||
public static class CachedEngineIntentBuilder {
|
||||
private final Class<? extends FlutterActivity> activityClass;
|
||||
private final String cachedEngineId;
|
||||
private boolean destroyEngineWithActivity = false;
|
||||
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
|
||||
|
||||
/**
|
||||
* Constructor that allows this {@code CachedEngineIntentBuilder} to be used by subclasses of
|
||||
* {@code FlutterActivity}.
|
||||
* <p>
|
||||
* 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.:
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* The default background mode is {@link BackgroundMode#opaque}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <item name="android:windowIsTranslucent">true</item>}.
|
||||
*/
|
||||
@NonNull
|
||||
public CachedEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
|
||||
this.backgroundMode = backgroundMode.name();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with
|
||||
* the desired configuration.
|
||||
*/
|
||||
@NonNull
|
||||
public Intent build(@NonNull Context context) {
|
||||
return new Intent(context, activityClass)
|
||||
.putExtra(EXTRA_CACHED_ENGINE_ID, cachedEngineId)
|
||||
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity)
|
||||
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode);
|
||||
}
|
||||
}
|
||||
@@ -522,6 +643,31 @@ public class FlutterActivity extends Activity
|
||||
return FlutterShellArgs.fromIntent(getIntent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of a statically cached {@link FlutterEngine} to use within this
|
||||
* {@code FlutterActivity}, or {@code null} if this {@code FlutterActivity} does not want to
|
||||
* use a cached {@link FlutterEngine}.
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public String getCachedEngineId() {
|
||||
return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the {@link FlutterEngine} backing this {@code FlutterActivity} should
|
||||
* outlive this {@code FlutterActivity}, or true to be destroyed when the {@code FlutterActivity}
|
||||
* is destroyed.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
@@ -753,16 +899,6 @@ public class FlutterActivity extends Activity
|
||||
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() {}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Arrays;
|
||||
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 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
/**
|
||||
* Obtains a reference to a FlutterEngine to back this delegate and its {@code host}.
|
||||
* <p>
|
||||
* First, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Second, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via
|
||||
* {@link Host#provideFlutterEngine(Context)}.
|
||||
* <p>
|
||||
* If the {@code host} does not provide a {@link FlutterEngine}, then a new {@link FlutterEngine}
|
||||
@@ -191,9 +196,21 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
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 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
* {@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 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
* if it was previously attached.</li>
|
||||
* <li>Destroys this delegate's {@link PlatformPlugin}.</li>
|
||||
* <li>Destroys this delegate's {@link FlutterEngine} if
|
||||
* {@link Host#retainFlutterEngineAfterHostDestruction()} returns false.</li>
|
||||
* {@link Host#shouldDestroyEngineWithHost()} ()} returns true.</li>
|
||||
* </ol>
|
||||
*/
|
||||
void onDetach() {
|
||||
@@ -412,8 +434,13 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
}
|
||||
|
||||
// Destroy our FlutterEngine if we're not set to retain it.
|
||||
if (!host.retainFlutterEngineAfterHostDestruction() && !isFlutterEngineFromHost) {
|
||||
if (host.shouldDestroyEngineWithHost()) {
|
||||
flutterEngine.destroy();
|
||||
|
||||
if (host.getCachedEngineId() != null) {
|
||||
FlutterEngineCache.getInstance().remove(host.getCachedEngineId());
|
||||
}
|
||||
|
||||
flutterEngine = null;
|
||||
}
|
||||
}
|
||||
@@ -587,6 +614,24 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
@NonNull
|
||||
FlutterShellArgs getFlutterShellArgs();
|
||||
|
||||
/**
|
||||
* Returns the ID of a statically cached {@link FlutterEngine} to use within this
|
||||
* delegate's host, or {@code null} if this delegate's host does not want to
|
||||
* use a cached {@link FlutterEngine}.
|
||||
*/
|
||||
@Nullable
|
||||
String getCachedEngineId();
|
||||
|
||||
/**
|
||||
* Returns true if the {@link FlutterEngine} used in this delegate should be destroyed
|
||||
* when the host/delegate are destroyed.
|
||||
* <p>
|
||||
* 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 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
|
||||
*/
|
||||
boolean shouldAttachEngineToActivity();
|
||||
|
||||
/**
|
||||
* Returns true if the {@link FlutterEngine} used in this delegate should outlive the
|
||||
* delegate.
|
||||
* <p>
|
||||
* 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.
|
||||
|
||||
@@ -47,6 +47,34 @@ import io.flutter.view.FlutterMain;
|
||||
* If convenient, consider using a {@link FlutterActivity} instead of a {@code FlutterFragment} to
|
||||
* avoid the work of forwarding calls.
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* 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:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>When {@code FlutterFragment} is in the first {@code Activity} displayed by the app, because
|
||||
* pre-warming a {@link FlutterEngine} would have no impact in this situation.</li>
|
||||
* <li>When you are unsure when/if you will need to display a Flutter experience.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The following illustrates how to pre-warm and cache a {@link FlutterEngine}:
|
||||
* <p>
|
||||
* {@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);
|
||||
* }
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* {@code FlutterFragment}'s default configuration creates a new {@link FlutterEngine} within
|
||||
* the {@code FlutterFragment} and uses the following settings:
|
||||
* <ul>
|
||||
* <li>Dart entrypoint: "main"</li>
|
||||
* <li>Initial route: "/"</li>
|
||||
* <li>Render mode: surface</li>
|
||||
* <li>Transparency mode: transparent</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* To use a new {@link FlutterEngine} with different settings, use {@link #withNewEngine()}.
|
||||
* <p>
|
||||
* To use a cached {@link FlutterEngine} instead of creating a new one, use
|
||||
* {@link #withCachedEngine(String)}.
|
||||
*/
|
||||
@NonNull
|
||||
public static FlutterFragment createDefaultFlutterFragment() {
|
||||
return new FlutterFragment.Builder().build();
|
||||
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 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}.
|
||||
* <p>
|
||||
* To create a {@code FlutterFragment} with default {@code arguments}, invoke
|
||||
* {@link #createDefaultFlutterFragment()}.
|
||||
* {@link #createDefault()}.
|
||||
* <p>
|
||||
* 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<MyFlutterFragment>();
|
||||
* }
|
||||
* <p>
|
||||
* 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:
|
||||
* <ol>
|
||||
* <li>Ensure the {@code FlutterFragment} subclass has a no-arg constructor.</li>
|
||||
* <li>Subclass this {@code Builder}.</li>
|
||||
* <li>Override the new {@code Builder}'s no-arg constructor and invoke the super constructor
|
||||
* to set the {@code FlutterFragment} subclass: {@code
|
||||
* <li>Subclass this {@code NewEngineFragmentBuilder}.</li>
|
||||
* <li>Override the new {@code NewEngineFragmentBuilder}'s no-arg constructor and invoke the
|
||||
* super constructor to set the {@code FlutterFragment} subclass: {@code
|
||||
* public MyBuilder() {
|
||||
* super(MyFlutterFragment.class);
|
||||
* }
|
||||
* }</li>
|
||||
* <li>Add appropriate property methods for the new properties.</li>
|
||||
* <li>Override {@link Builder#createArgs()}, call through to the super method, then add
|
||||
* the new properties as arguments in the {@link Bundle}.</li>
|
||||
* <li>Override {@link NewEngineFragmentBuilder#createArgs()}, call through to the super method,
|
||||
* then add the new properties as arguments in the {@link Bundle}.</li>
|
||||
* </ol>
|
||||
* 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 class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* .build<MyFlutterFragment>();
|
||||
* }
|
||||
*/
|
||||
public static class Builder {
|
||||
public static class NewEngineFragmentBuilder {
|
||||
private final Class<? extends FlutterFragment> fragmentClass;
|
||||
private String dartEntrypoint = "main";
|
||||
private String initialRoute = "/";
|
||||
@@ -142,18 +208,18 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
private boolean shouldAttachEngineToActivity = true;
|
||||
|
||||
/**
|
||||
* Constructs a {@code Builder} that is configured to construct an instance of
|
||||
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
|
||||
* {@code FlutterFragment}.
|
||||
*/
|
||||
public Builder() {
|
||||
public NewEngineFragmentBuilder() {
|
||||
fragmentClass = FlutterFragment.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code Builder} that is configured to construct an instance of
|
||||
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
|
||||
* {@code subclass}, which extends {@code FlutterFragment}.
|
||||
*/
|
||||
public Builder(@NonNull Class<? extends FlutterFragment> subclass) {
|
||||
public NewEngineFragmentBuilder(@NonNull Class<? extends FlutterFragment> subclass) {
|
||||
fragmentClass = subclass;
|
||||
}
|
||||
|
||||
@@ -161,7 +227,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* The name of the initial Dart method to invoke, defaults to "main".
|
||||
*/
|
||||
@NonNull
|
||||
public Builder dartEntrypoint(@NonNull String dartEntrypoint) {
|
||||
public NewEngineFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
|
||||
this.dartEntrypoint = dartEntrypoint;
|
||||
return this;
|
||||
}
|
||||
@@ -171,7 +237,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* defaults to "/".
|
||||
*/
|
||||
@NonNull
|
||||
public Builder initialRoute(@NonNull String initialRoute) {
|
||||
public NewEngineFragmentBuilder initialRoute(@NonNull String initialRoute) {
|
||||
this.initialRoute = initialRoute;
|
||||
return this;
|
||||
}
|
||||
@@ -181,7 +247,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* to {@link FlutterMain#findAppBundlePath()}
|
||||
*/
|
||||
@NonNull
|
||||
public Builder appBundlePath(@NonNull String appBundlePath) {
|
||||
public NewEngineFragmentBuilder appBundlePath(@NonNull String appBundlePath) {
|
||||
this.appBundlePath = appBundlePath;
|
||||
return this;
|
||||
}
|
||||
@@ -190,7 +256,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* Any special configuration arguments for the Flutter engine
|
||||
*/
|
||||
@NonNull
|
||||
public Builder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) {
|
||||
public NewEngineFragmentBuilder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) {
|
||||
this.shellArgs = shellArgs;
|
||||
return this;
|
||||
}
|
||||
@@ -204,7 +270,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* cannot.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder renderMode(@NonNull FlutterView.RenderMode renderMode) {
|
||||
public NewEngineFragmentBuilder renderMode(@NonNull FlutterView.RenderMode renderMode) {
|
||||
this.renderMode = renderMode;
|
||||
return this;
|
||||
}
|
||||
@@ -216,7 +282,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* See {@link FlutterView.TransparencyMode} for implications of this selection.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) {
|
||||
public NewEngineFragmentBuilder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) {
|
||||
this.transparencyMode = transparencyMode;
|
||||
return this;
|
||||
}
|
||||
@@ -256,7 +322,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
* setting {@code shouldAttachEngineToActivity} to {@code false}.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) {
|
||||
public NewEngineFragmentBuilder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) {
|
||||
this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
|
||||
return this;
|
||||
}
|
||||
@@ -280,6 +346,7 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
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);
|
||||
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -307,6 +374,194 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CachedEngineFragmentBuilder} to create a {@code FlutterFragment} with a cached
|
||||
* {@link FlutterEngine} in {@link FlutterEngineCache}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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<MyFlutterFragment>();
|
||||
* }
|
||||
* <p>
|
||||
* Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
|
||||
* {@code CachedEngineFragmentBuilder} to add the new properties:
|
||||
* <ol>
|
||||
* <li>Ensure the {@code FlutterFragment} subclass has a no-arg constructor.</li>
|
||||
* <li>Subclass this {@code CachedEngineFragmentBuilder}.</li>
|
||||
* <li>Override the new {@code CachedEngineFragmentBuilder}'s no-arg constructor and invoke the
|
||||
* super constructor to set the {@code FlutterFragment} subclass: {@code
|
||||
* public MyBuilder() {
|
||||
* super(MyFlutterFragment.class);
|
||||
* }
|
||||
* }</li>
|
||||
* <li>Add appropriate property methods for the new properties.</li>
|
||||
* <li>Override {@link CachedEngineFragmentBuilder#createArgs()}, call through to the super
|
||||
* method, then add the new properties as arguments in the {@link Bundle}.</li>
|
||||
* </ol>
|
||||
* Once a {@code CachedEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment}
|
||||
* subclass can be instantiated as follows.
|
||||
* {@code
|
||||
* MyFlutterFragment f = new MyBuilder()
|
||||
* .someExistingProperty(...)
|
||||
* .someNewProperty(...)
|
||||
* .build<MyFlutterFragment>();
|
||||
* }
|
||||
*/
|
||||
public static class CachedEngineFragmentBuilder {
|
||||
private final Class<? extends FlutterFragment> fragmentClass;
|
||||
private final String engineId;
|
||||
private boolean destroyEngineWithFragment = false;
|
||||
private FlutterView.RenderMode renderMode = FlutterView.RenderMode.surface;
|
||||
private FlutterView.TransparencyMode transparencyMode = FlutterView.TransparencyMode.transparent;
|
||||
private boolean shouldAttachEngineToActivity = true;
|
||||
|
||||
private CachedEngineFragmentBuilder(@NonNull String engineId) {
|
||||
this(FlutterFragment.class, engineId);
|
||||
}
|
||||
|
||||
protected CachedEngineFragmentBuilder(@NonNull Class<? extends FlutterFragment> subclass, @NonNull String engineId) {
|
||||
this.fragmentClass = subclass;
|
||||
this.engineId = engineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass {@code true} to destroy the cached {@link FlutterEngine} when this
|
||||
* {@code FlutterFragment} is destroyed, or {@code false} for the cached {@link FlutterEngine}
|
||||
* to outlive this {@code FlutterFragment}.
|
||||
*/
|
||||
@NonNull
|
||||
public CachedEngineFragmentBuilder destroyEngineWithFragment(boolean destroyEngineWithFragment) {
|
||||
this.destroyEngineWithFragment = destroyEngineWithFragment;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Flutter either as a {@link FlutterView.RenderMode#surface} or a
|
||||
* {@link FlutterView.RenderMode#texture}. You should use {@code surface} unless
|
||||
* you have a specific reason to use {@code texture}. {@code texture} comes with
|
||||
* a significant performance impact, but {@code texture} can be displayed
|
||||
* beneath other Android {@code View}s and animated, whereas {@code surface}
|
||||
* cannot.
|
||||
*/
|
||||
@NonNull
|
||||
public CachedEngineFragmentBuilder renderMode(@NonNull FlutterView.RenderMode renderMode) {
|
||||
this.renderMode = renderMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Support a {@link FlutterView.TransparencyMode#transparent} background within {@link FlutterView},
|
||||
* or force an {@link FlutterView.TransparencyMode#opaque} background.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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()}.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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 <T extends FlutterFragment> T build() {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
T frag = (T) fragmentClass.getDeclaredConstructor().newInstance();
|
||||
if (frag == null) {
|
||||
throw new RuntimeException("The FlutterFragment subclass sent in the constructor ("
|
||||
+ fragmentClass.getCanonicalName() + ") does not match the expected return type.");
|
||||
}
|
||||
|
||||
Bundle args = createArgs();
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not instantiate FlutterFragment subclass (" + fragmentClass.getName() + ")", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate that runs all lifecycle and OS hook logic that is common between
|
||||
// FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate
|
||||
// implementation for details about why it exists.
|
||||
@@ -494,6 +749,29 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of a statically cached {@link FlutterEngine} to use within this
|
||||
* {@code FlutterFragment}, or {@code null} if this {@code FlutterFragment} does not want to
|
||||
* use a cached {@link FlutterEngine}.
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getCachedEngineId() {
|
||||
return getArguments().getString(ARG_CACHED_ENGINE_ID, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive
|
||||
* the {@code FlutterFragment}, itself.
|
||||
* <p>
|
||||
* 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 class FlutterFragment extends Fragment implements FlutterActivityAndFragm
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Builder#shouldAttachEngineToActivity()}.
|
||||
* See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and
|
||||
* {@link CachedEngineFragmentBuilder#shouldAttachEngineToActivity()}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Defaults to false. This method can be overridden in subclasses to retain the
|
||||
* {@link FlutterEngine}.
|
||||
* <p>
|
||||
* 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* The ID of a given {@link FlutterEngine} can be whatever {@code String} is desired.
|
||||
* <p>
|
||||
* {@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}.
|
||||
* <p>
|
||||
* 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<String, FlutterEngine> cachedEngines = new HashMap<>();
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ FlutterEngineCache() {}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if a {@link FlutterEngine} in this cache is associated with the
|
||||
* given {@code engineId}.
|
||||
*/
|
||||
public boolean contains(@NonNull String engineId) {
|
||||
return cachedEngines.containsKey(engineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FlutterEngine} in this cache that is associated with the given
|
||||
* {@code engineId}, or {@code null} is no such {@link FlutterEngine} exists.
|
||||
*/
|
||||
@Nullable
|
||||
public FlutterEngine get(@NonNull String engineId) {
|
||||
return cachedEngines.get(engineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the given {@link FlutterEngine} in this cache and associates it with the given
|
||||
* {@code engineId}.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.arch.lifecycle.Lifecycle;
|
||||
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.RuntimeEnvironment;
|
||||
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.LocalizationChannel;
|
||||
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 @@ import static org.mockito.Mockito.when;
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
// 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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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 class FlutterActivityAndFragmentDelegateTest {
|
||||
);
|
||||
|
||||
// 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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
@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 class FlutterActivityAndFragmentDelegateTest {
|
||||
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}.
|
||||
* <p>
|
||||
@@ -387,115 +551,4 @@ public class FlutterActivityAndFragmentDelegateTest {
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link FlutterActivityAndFragmentDelegate.Host} that returns values desired by this
|
||||
* test suite.
|
||||
* <p>
|
||||
* 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() {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FlutterActivity> activityController = Robolectric.buildActivity(FlutterActivity.class, intent);
|
||||
FlutterActivity flutterActivity = activityController.get();
|
||||
|
||||
assertEquals("main", flutterActivity.getDartEntrypointFunctionName());
|
||||
assertEquals("/", flutterActivity.getInitialRoute());
|
||||
assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray());
|
||||
assertTrue(flutterActivity.shouldAttachEngineToActivity());
|
||||
assertNull(flutterActivity.getCachedEngineId());
|
||||
assertTrue(flutterActivity.shouldDestroyEngineWithHost());
|
||||
assertEquals(FlutterActivity.BackgroundMode.opaque, flutterActivity.getBackgroundMode());
|
||||
assertEquals(FlutterView.RenderMode.surface, flutterActivity.getRenderMode());
|
||||
assertEquals(FlutterView.TransparencyMode.opaque, flutterActivity.getTransparencyMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesNewEngineIntentWithRequestedSettings() {
|
||||
Intent intent = FlutterActivity.withNewEngine()
|
||||
.dartEntrypoint("custom_entrypoint")
|
||||
.initialRoute("/custom/route")
|
||||
.backgroundMode(FlutterActivity.BackgroundMode.transparent)
|
||||
.build(RuntimeEnvironment.application);
|
||||
ActivityController<FlutterActivity> activityController = Robolectric.buildActivity(FlutterActivity.class, intent);
|
||||
FlutterActivity flutterActivity = activityController.get();
|
||||
|
||||
assertEquals("custom_entrypoint", flutterActivity.getDartEntrypointFunctionName());
|
||||
assertEquals("/custom/route", flutterActivity.getInitialRoute());
|
||||
assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray());
|
||||
assertTrue(flutterActivity.shouldAttachEngineToActivity());
|
||||
assertNull(flutterActivity.getCachedEngineId());
|
||||
assertTrue(flutterActivity.shouldDestroyEngineWithHost());
|
||||
assertEquals(FlutterActivity.BackgroundMode.transparent, flutterActivity.getBackgroundMode());
|
||||
assertEquals(FlutterView.RenderMode.texture, flutterActivity.getRenderMode());
|
||||
assertEquals(FlutterView.TransparencyMode.transparent, flutterActivity.getTransparencyMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesCachedEngineIntentThatDoesNotDestroyTheEngine() {
|
||||
Intent intent = FlutterActivity.withCachedEngine("my_cached_engine")
|
||||
.destroyEngineWithActivity(false)
|
||||
.build(RuntimeEnvironment.application);
|
||||
ActivityController<FlutterActivity> activityController = Robolectric.buildActivity(FlutterActivity.class, intent);
|
||||
FlutterActivity flutterActivity = activityController.get();
|
||||
|
||||
assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray());
|
||||
assertTrue(flutterActivity.shouldAttachEngineToActivity());
|
||||
assertEquals("my_cached_engine", flutterActivity.getCachedEngineId());
|
||||
assertFalse(flutterActivity.shouldDestroyEngineWithHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesCachedEngineIntentThatDestroysTheEngine() {
|
||||
Intent intent = FlutterActivity.withCachedEngine("my_cached_engine")
|
||||
.destroyEngineWithActivity(true)
|
||||
.build(RuntimeEnvironment.application);
|
||||
ActivityController<FlutterActivity> activityController = Robolectric.buildActivity(FlutterActivity.class, intent);
|
||||
FlutterActivity flutterActivity = activityController.get();
|
||||
|
||||
assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray());
|
||||
assertTrue(flutterActivity.shouldAttachEngineToActivity());
|
||||
assertEquals("my_cached_engine", flutterActivity.getCachedEngineId());
|
||||
assertTrue(flutterActivity.shouldDestroyEngineWithHost());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.flutter.embedding.android;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
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 FlutterFragmentTest {
|
||||
@Test
|
||||
public void itCreatesDefaultFragmentWithExpectedDefaults() {
|
||||
FlutterFragment fragment = FlutterFragment.createDefault();
|
||||
|
||||
assertEquals("main", fragment.getDartEntrypointFunctionName());
|
||||
assertEquals("/", fragment.getInitialRoute());
|
||||
assertArrayEquals(new String[]{}, fragment.getFlutterShellArgs().toArray());
|
||||
assertTrue(fragment.shouldAttachEngineToActivity());
|
||||
assertNull(fragment.getCachedEngineId());
|
||||
assertTrue(fragment.shouldDestroyEngineWithHost());
|
||||
assertEquals(FlutterView.RenderMode.surface, fragment.getRenderMode());
|
||||
assertEquals(FlutterView.TransparencyMode.transparent, fragment.getTransparencyMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesNewEngineFragmentWithRequestedSettings() {
|
||||
FlutterFragment fragment = FlutterFragment.withNewEngine()
|
||||
.dartEntrypoint("custom_entrypoint")
|
||||
.initialRoute("/custom/route")
|
||||
.shouldAttachEngineToActivity(false)
|
||||
.renderMode(FlutterView.RenderMode.texture)
|
||||
.transparencyMode(FlutterView.TransparencyMode.opaque)
|
||||
.build();
|
||||
|
||||
assertEquals("custom_entrypoint", fragment.getDartEntrypointFunctionName());
|
||||
assertEquals("/custom/route", fragment.getInitialRoute());
|
||||
assertArrayEquals(new String[]{}, fragment.getFlutterShellArgs().toArray());
|
||||
assertFalse(fragment.shouldAttachEngineToActivity());
|
||||
assertNull(fragment.getCachedEngineId());
|
||||
assertTrue(fragment.shouldDestroyEngineWithHost());
|
||||
assertEquals(FlutterView.RenderMode.texture, fragment.getRenderMode());
|
||||
assertEquals(FlutterView.TransparencyMode.opaque, fragment.getTransparencyMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesCachedEngineFragmentThatDoesNotDestroyTheEngine() {
|
||||
FlutterFragment fragment = FlutterFragment
|
||||
.withCachedEngine("my_cached_engine")
|
||||
.build();
|
||||
|
||||
assertTrue(fragment.shouldAttachEngineToActivity());
|
||||
assertEquals("my_cached_engine", fragment.getCachedEngineId());
|
||||
assertFalse(fragment.shouldDestroyEngineWithHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCreatesCachedEngineFragmentThatDestroysTheEngine() {
|
||||
FlutterFragment fragment = FlutterFragment
|
||||
.withCachedEngine("my_cached_engine")
|
||||
.destroyEngineWithFragment(true)
|
||||
.build();
|
||||
|
||||
assertTrue(fragment.shouldAttachEngineToActivity());
|
||||
assertEquals("my_cached_engine", fragment.getCachedEngineId());
|
||||
assertTrue(fragment.shouldDestroyEngineWithHost());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.flutter.embedding.engine;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@Config(manifest=Config.NONE)
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class FlutterEngineCacheTest {
|
||||
@Test
|
||||
public void itHoldsFlutterEngines() {
|
||||
// --- Test Setup ---
|
||||
FlutterEngine flutterEngine = mock(FlutterEngine.class);
|
||||
FlutterEngineCache cache = new FlutterEngineCache();
|
||||
|
||||
// --- Execute Test ---
|
||||
cache.put("my_flutter_engine", flutterEngine);
|
||||
|
||||
// --- Verify Results ---
|
||||
assertEquals(flutterEngine, cache.get("my_flutter_engine"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itQueriesFlutterEngineExistence() {
|
||||
// --- Test Setup ---
|
||||
FlutterEngine flutterEngine = mock(FlutterEngine.class);
|
||||
FlutterEngineCache cache = new FlutterEngineCache();
|
||||
|
||||
// --- Execute Test ---
|
||||
assertFalse(cache.contains("my_flutter_engine"));
|
||||
|
||||
cache.put("my_flutter_engine", flutterEngine);
|
||||
|
||||
// --- Verify Results ---
|
||||
assertTrue(cache.contains("my_flutter_engine"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itRemovesFlutterEngines() {
|
||||
// --- Test Setup ---
|
||||
FlutterEngine flutterEngine = mock(FlutterEngine.class);
|
||||
FlutterEngineCache cache = new FlutterEngineCache();
|
||||
|
||||
// --- Execute Test ---
|
||||
cache.put("my_flutter_engine", flutterEngine);
|
||||
cache.remove("my_flutter_engine");
|
||||
|
||||
// --- Verify Results ---
|
||||
assertNull(cache.get("my_flutter_engine"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user