blob: 0ef5d872358d918000bc43d117e9f729fb41b6e0 [file] [log] [blame]
// 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.android;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.ViewUtils;
import java.util.ArrayList;
import java.util.List;
/**
* {@code Fragment} which displays a Flutter UI that takes up all available {@code Fragment} space.
*
* <p>Using a {@code FlutterFragment} requires forwarding a number of calls from an {@code Activity}
* to ensure that the internal Flutter app behaves as expected:
*
* <ol>
* <li>{@link #onPostResume()}
* <li>{@link #onBackPressed()}
* <li>{@link #onRequestPermissionsResult(int, String[], int[])}
* <li>{@link #onNewIntent(Intent)}
* <li>{@link #onUserLeaveHint()}
* </ol>
*
* {@link #onBackPressed()} does not need to be called through if the fragment is constructed by one
* of the builders with {@code shouldAutomaticallyHandleOnBackPressed(true)}.
*
* <p>Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be
* sure to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than {@link
* android.app.Activity#startActivityForResult(Intent, int)}. If the {@code Activity} version of the
* method is invoked then this {@code Fragment} will never receive its {@link
* Fragment#onActivityResult(int, int, Intent)} callback.
*
* <p>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
* io.flutter.embedding.engine.FlutterEngine}. To use a cached {@link
* io.flutter.embedding.engine.FlutterEngine}, ensure that the {@link
* io.flutter.embedding.engine.FlutterEngine} is stored in {@link
* io.flutter.embedding.engine.FlutterEngineCache} and then use {@link #withCachedEngine(String)} to
* build a {@code FlutterFragment} with the cached {@link
* io.flutter.embedding.engine.FlutterEngine}'s ID.
*
* <p>It is generally recommended to use a cached {@link io.flutter.embedding.engine.FlutterEngine}
* to avoid a momentary delay when initializing a new {@link
* io.flutter.embedding.engine.FlutterEngine}. The two exceptions to using a cached {@link
* FlutterEngine} are:
*
* <ul>
* <li>When {@code FlutterFragment} is in the first {@code Activity} displayed by the app, because
* pre-warming a {@link io.flutter.embedding.engine.FlutterEngine} would have no impact in
* this situation.
* <li>When you are unsure when/if you will need to display a Flutter experience.
* </ul>
*
* <p>The following illustrates how to pre-warm and cache a {@link
* io.flutter.embedding.engine.FlutterEngine}:
*
* <pre>{@code
* // Create and pre-warm a FlutterEngine.
* FlutterEngineGroup group = new FlutterEngineGroup(context);
* FlutterEngine flutterEngine = group.createAndRunDefaultEngine(context);
* flutterEngine
* .getDartExecutor()
* .executeDartEntrypoint(DartEntrypoint.createDefault());
*
* // Cache the pre-warmed FlutterEngine in the FlutterEngineCache.
* FlutterEngineCache.getInstance().put("my_engine", flutterEngine);
* }</pre>
*
* <p>If Flutter is needed in a location that can only use a {@code View}, consider using a {@link
* io.flutter.embedding.android.FlutterView}. Using a {@link
* io.flutter.embedding.android.FlutterView} requires forwarding some calls from an {@code
* Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a {@code Fragment}.
*/
public class FlutterFragment extends Fragment
implements FlutterActivityAndFragmentDelegate.Host,
ComponentCallbacks2,
FlutterActivityAndFragmentDelegate.DelegateFactory {
/**
* The ID of the {@code FlutterView} created by this activity.
*
* <p>This ID can be used to lookup {@code FlutterView} in the Android view hierarchy. For more,
* see {@link android.view.View#findViewById}.
*/
public static final int FLUTTER_VIEW_ID = ViewUtils.generateViewId(0xF1F2);
private static final String TAG = "FlutterFragment";
/** The Dart entrypoint method name that is executed upon initialization. */
protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint";
/** The Dart entrypoint method's URI that is executed upon initialization. */
protected static final String ARG_DART_ENTRYPOINT_URI = "dart_entrypoint_uri";
/** The Dart entrypoint arguments that is executed upon initialization. */
protected static final String ARG_DART_ENTRYPOINT_ARGS = "dart_entrypoint_args";
/** Initial Flutter route that is rendered in a Navigator widget. */
protected static final String ARG_INITIAL_ROUTE = "initial_route";
/** Whether the activity delegate should handle the deeplinking request. */
protected static final String ARG_HANDLE_DEEPLINKING = "handle_deeplinking";
/** Path to Flutter's Dart code. */
protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path";
/** Whether to delay the Android drawing pass till after the Flutter UI has been displayed. */
protected static final String ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW =
"should_delay_first_android_view_draw";
/** Flutter shell arguments. */
protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args";
/**
* {@link RenderMode} to be used for the {@link io.flutter.embedding.android.FlutterView} in this
* {@code FlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode";
/**
* {@link TransparencyMode} to be used for the {@link io.flutter.embedding.android.FlutterView} in
* this {@code FlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode";
/** See {@link #shouldAttachEngineToActivity()}. */
protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY =
"should_attach_engine_to_activity";
/**
* The ID of a {@link io.flutter.embedding.engine.FlutterEngine} cached in {@link
* io.flutter.embedding.engine.FlutterEngineCache} that will be used within the created {@code
* FlutterFragment}.
*/
protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id";
protected static final String ARG_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id";
/**
* True if the {@link io.flutter.embedding.engine.FlutterEngine} in the created {@code
* FlutterFragment} should be destroyed when the {@code FlutterFragment} is destroyed, false if
* the {@link io.flutter.embedding.engine.FlutterEngine} should outlive the {@code
* FlutterFragment}.
*/
protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment";
/**
* True if the framework state in the engine attached to this engine should be stored and restored
* when this fragment is created and destroyed.
*/
protected static final String ARG_ENABLE_STATE_RESTORATION = "enable_state_restoration";
/**
* True if the fragment should receive {@link #onBackPressed()} events automatically, without
* requiring an explicit activity call through.
*/
protected static final String ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED =
"should_automatically_handle_on_back_pressed";
/**
* Creates a {@code FlutterFragment} with a default configuration.
*
* <p>{@code FlutterFragment}'s default configuration creates a new {@link
* io.flutter.embedding.engine.FlutterEngine} within the {@code FlutterFragment} and uses the
* following settings:
*
* <ul>
* <li>Dart entrypoint: "main"
* <li>Initial route: "/"
* <li>Render mode: surface
* <li>Transparency mode: transparent
* </ul>
*
* <p>To use a new {@link io.flutter.embedding.engine.FlutterEngine} with different settings, use
* {@link #withNewEngine()}.
*
* <p>To use a cached {@link io.flutter.embedding.engine.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 io.flutter.embedding.engine.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 NewEngineFragmentBuilder}.
*
* <p>To create a {@code FlutterFragment} with default {@code arguments}, invoke {@link
* #createDefault()}.
*
* <p>Subclasses of {@code FlutterFragment} that do not introduce any new arguments can use this
* {@code NewEngineFragmentBuilder} to construct instances of the subclass without subclassing
* this {@code NewEngineFragmentBuilder}. {@code MyFlutterFragment f = new
* FlutterFragment.NewEngineFragmentBuilder(MyFlutterFragment.class) .someProperty(...)
* .someOtherProperty(...) .build<MyFlutterFragment>(); }
*
* <p>Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
* {@code NewEngineFragmentBuilder} to add the new properties:
*
* <ol>
* <li>Ensure the {@code FlutterFragment} subclass has a no-arg constructor.
* <li>Subclass this {@code NewEngineFragmentBuilder}.
* <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>Add appropriate property methods for the new properties.
* <li>Override {@link NewEngineFragmentBuilder#createArgs()}, call through to the super method,
* then add the new properties as arguments in the {@link Bundle}.
* </ol>
*
* Once a {@code NewEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment}
* subclass can be instantiated as follows. {@code MyFlutterFragment f = new MyBuilder()
* .someExistingProperty(...) .someNewProperty(...) .build<MyFlutterFragment>(); }
*/
public static class NewEngineFragmentBuilder {
private final Class<? extends FlutterFragment> fragmentClass;
private String dartEntrypoint = "main";
private String dartLibraryUri = null;
private List<String> dartEntrypointArgs;
private String initialRoute = "/";
private boolean handleDeeplinking = false;
private String appBundlePath = null;
private FlutterShellArgs shellArgs = null;
private RenderMode renderMode = RenderMode.surface;
private TransparencyMode transparencyMode = TransparencyMode.transparent;
private boolean shouldAttachEngineToActivity = true;
private boolean shouldAutomaticallyHandleOnBackPressed = false;
private boolean shouldDelayFirstAndroidViewDraw = false;
/**
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
* {@code FlutterFragment}.
*/
public NewEngineFragmentBuilder() {
fragmentClass = FlutterFragment.class;
}
/**
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
* {@code subclass}, which extends {@code FlutterFragment}.
*/
public NewEngineFragmentBuilder(@NonNull Class<? extends FlutterFragment> subclass) {
fragmentClass = subclass;
}
/** The name of the initial Dart method to invoke, defaults to "main". */
@NonNull
public NewEngineFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
this.dartEntrypoint = dartEntrypoint;
return this;
}
@NonNull
public NewEngineFragmentBuilder dartLibraryUri(@NonNull String dartLibraryUri) {
this.dartLibraryUri = dartLibraryUri;
return this;
}
/** Arguments passed as a list of string to Dart's entrypoint function. */
@NonNull
public NewEngineFragmentBuilder dartEntrypointArgs(@NonNull List<String> dartEntrypointArgs) {
this.dartEntrypointArgs = dartEntrypointArgs;
return this;
}
/**
* The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to
* "/".
*/
@NonNull
public NewEngineFragmentBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
/**
* Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
* getInitialRoute} returns null.
*/
@NonNull
public NewEngineFragmentBuilder handleDeeplinking(@NonNull Boolean handleDeeplinking) {
this.handleDeeplinking = handleDeeplinking;
return this;
}
/**
* The path to the app bundle which contains the Dart app to execute. Null when unspecified,
* which defaults to {@link
* io.flutter.embedding.engine.loader.FlutterLoader#findAppBundlePath()}
*/
@NonNull
public NewEngineFragmentBuilder appBundlePath(@NonNull String appBundlePath) {
this.appBundlePath = appBundlePath;
return this;
}
/** Any special configuration arguments for the Flutter engine */
@NonNull
public NewEngineFragmentBuilder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) {
this.shellArgs = shellArgs;
return this;
}
/**
* Render Flutter either as a {@link RenderMode#surface} or a {@link 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 NewEngineFragmentBuilder renderMode(@NonNull RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
/**
* Support a {@link TransparencyMode#transparent} background within {@link
* io.flutter.embedding.android.FlutterView}, or force an {@link TransparencyMode#opaque}
* background.
*
* <p>See {@link TransparencyMode} for implications of this selection.
*/
@NonNull
public NewEngineFragmentBuilder transparencyMode(@NonNull TransparencyMode transparencyMode) {
this.transparencyMode = transparencyMode;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically attach its {@code Activity}
* as a control surface for its {@link io.flutter.embedding.engine.FlutterEngine}.
*
* <p>Control surfaces are used to provide Android resources and lifecycle events to plugins
* that are attached to the {@link io.flutter.embedding.engine.FlutterEngine}. If {@code
* shouldAttachEngineToActivity} is true then this {@code FlutterFragment} will connect its
* {@link io.flutter.embedding.engine.FlutterEngine} to the surrounding {@code Activity}, along
* with any plugins that are registered with that {@link FlutterEngine}. This allows plugins to
* access the {@code Activity}, as well as receive {@code Activity}-specific calls, e.g., {@link
* android.app.Activity#onNewIntent(Intent)}. If {@code shouldAttachEngineToActivity} is false,
* then this {@code FlutterFragment} will not automatically manage the connection between its
* {@link io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. The
* {@code Activity} will need to be manually connected to this {@code FlutterFragment}'s {@link
* io.flutter.embedding.engine.FlutterEngine} by the app developer. See {@link
* FlutterEngine#getActivityControlSurface()}.
*
* <p>One reason that a developer might choose to manually manage the relationship between the
* {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the developer
* wants to move the {@link FlutterEngine} somewhere else. For example, a developer might want
* the {@link io.flutter.embedding.engine.FlutterEngine} to outlive the surrounding {@code
* Activity} so that it can be used later in a different {@code Activity}. To accomplish this,
* the {@link io.flutter.embedding.engine.FlutterEngine} will need to be disconnected from the
* surrounding {@code Activity} at an unusual time, preventing this {@code FlutterFragment} from
* correctly managing the relationship between the {@link
* io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}.
*
* <p>Another reason that a developer might choose to manually manage the relationship between
* the {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the
* developer wants to prevent, or explicitly control when the {@link
* io.flutter.embedding.engine.FlutterEngine}'s plugins have access to the surrounding {@code
* Activity}. For example, imagine that this {@code FlutterFragment} only takes up part of the
* screen and the app developer wants to ensure that none of the Flutter plugins are able to
* manipulate the surrounding {@code Activity}. In this case, the developer would not want the
* {@link io.flutter.embedding.engine.FlutterEngine} to have access to the {@code Activity},
* which can be accomplished by setting {@code shouldAttachEngineToActivity} to {@code false}.
*/
@NonNull
public NewEngineFragmentBuilder shouldAttachEngineToActivity(
boolean shouldAttachEngineToActivity) {
this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically receive {@link
* #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled
* by default.
*
* <p>When enabled, the activity will automatically dispatch back-press events to the fragment's
* {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link
* #onBackPressed()} in client code. If enabled, do <b>not</b> invoke {@link #onBackPressed()}
* manually.
*
* <p>This behavior relies on the implementation of {@link #popSystemNavigator()}. It's not
* recommended to override that method when enabling this attribute, but if you do, you should
* always fall back to calling {@code super.popSystemNavigator()} when not relying on custom
* behavior.
*/
@NonNull
public NewEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
boolean shouldAutomaticallyHandleOnBackPressed) {
this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
return this;
}
/**
* Whether to delay the Android drawing pass till after the Flutter UI has been displayed.
*
* <p>See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details.
*/
@NonNull
public NewEngineFragmentBuilder shouldDelayFirstAndroidViewDraw(
boolean shouldDelayFirstAndroidViewDraw) {
this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw;
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_INITIAL_ROUTE, initialRoute);
args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
args.putString(ARG_APP_BUNDLE_PATH, appBundlePath);
args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint);
args.putString(ARG_DART_ENTRYPOINT_URI, dartLibraryUri);
args.putStringArrayList(
ARG_DART_ENTRYPOINT_ARGS,
dartEntrypointArgs != null ? new ArrayList(dartEntrypointArgs) : null);
// TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of
// conflating.
if (null != shellArgs) {
args.putStringArray(ARG_FLUTTER_INITIALIZATION_ARGS, shellArgs.toArray());
}
args.putString(
ARG_FLUTTERVIEW_RENDER_MODE,
renderMode != null ? renderMode.name() : RenderMode.surface.name());
args.putString(
ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name());
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
args.putBoolean(
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw);
return args;
}
/**
* Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on
* properties set on this {@code Builder}.
*/
@NonNull
public <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);
}
}
}
/**
* Returns a {@link CachedEngineFragmentBuilder} to create a {@code FlutterFragment} with a cached
* {@link io.flutter.embedding.engine.FlutterEngine} in {@link
* io.flutter.embedding.engine.FlutterEngineCache}.
*
* <p>An {@code IllegalStateException} will be thrown during the lifecycle of the {@code
* FlutterFragment} if a cached {@link io.flutter.embedding.engine.FlutterEngine} is requested but
* does not exist in the cache.
*
* <p>To create a {@code FlutterFragment} that uses a new {@link
* io.flutter.embedding.engine.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
* io.flutter.embedding.engine.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>Subclass this {@code CachedEngineFragmentBuilder}.
* <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>Add appropriate property methods for the new properties.
* <li>Override {@link CachedEngineFragmentBuilder#createArgs()}, call through to the super
* method, then add the new properties as arguments in the {@link Bundle}.
* </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 boolean handleDeeplinking = false;
private RenderMode renderMode = RenderMode.surface;
private TransparencyMode transparencyMode = TransparencyMode.transparent;
private boolean shouldAttachEngineToActivity = true;
private boolean shouldAutomaticallyHandleOnBackPressed = false;
private boolean shouldDelayFirstAndroidViewDraw = false;
private CachedEngineFragmentBuilder(@NonNull String engineId) {
this(FlutterFragment.class, engineId);
}
public CachedEngineFragmentBuilder(
@NonNull Class<? extends FlutterFragment> subclass, @NonNull String engineId) {
this.fragmentClass = subclass;
this.engineId = engineId;
}
/**
* Pass {@code true} to destroy the cached {@link io.flutter.embedding.engine.FlutterEngine}
* when this {@code FlutterFragment} is destroyed, or {@code false} for the cached {@link
* io.flutter.embedding.engine.FlutterEngine} to outlive this {@code FlutterFragment}.
*/
@NonNull
public CachedEngineFragmentBuilder destroyEngineWithFragment(
boolean destroyEngineWithFragment) {
this.destroyEngineWithFragment = destroyEngineWithFragment;
return this;
}
/**
* Render Flutter either as a {@link RenderMode#surface} or a {@link 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 RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
/**
* Support a {@link TransparencyMode#transparent} background within {@link
* io.flutter.embedding.android.FlutterView}, or force an {@link TransparencyMode#opaque}
* background.
*
* <p>See {@link TransparencyMode} for implications of this selection.
*/
@NonNull
public CachedEngineFragmentBuilder transparencyMode(
@NonNull TransparencyMode transparencyMode) {
this.transparencyMode = transparencyMode;
return this;
}
/**
* Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
* getInitialRoute} returns null.
*/
@NonNull
public CachedEngineFragmentBuilder handleDeeplinking(@NonNull Boolean handleDeeplinking) {
this.handleDeeplinking = handleDeeplinking;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically attach its {@code Activity}
* as a control surface for its {@link io.flutter.embedding.engine.FlutterEngine}.
*
* <p>Control surfaces are used to provide Android resources and lifecycle events to plugins
* that are attached to the {@link io.flutter.embedding.engine.FlutterEngine}. If {@code
* shouldAttachEngineToActivity} is true then this {@code FlutterFragment} will connect its
* {@link io.flutter.embedding.engine.FlutterEngine} to the surrounding {@code Activity}, along
* with any plugins that are registered with that {@link FlutterEngine}. This allows plugins to
* access the {@code Activity}, as well as receive {@code Activity}-specific calls, e.g., {@link
* android.app.Activity#onNewIntent(Intent)}. If {@code shouldAttachEngineToActivity} is false,
* then this {@code FlutterFragment} will not automatically manage the connection between its
* {@link io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. The
* {@code Activity} will need to be manually connected to this {@code FlutterFragment}'s {@link
* io.flutter.embedding.engine.FlutterEngine} by the app developer. See {@link
* FlutterEngine#getActivityControlSurface()}.
*
* <p>One reason that a developer might choose to manually manage the relationship between the
* {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the developer
* wants to move the {@link FlutterEngine} somewhere else. For example, a developer might want
* the {@link io.flutter.embedding.engine.FlutterEngine} to outlive the surrounding {@code
* Activity} so that it can be used later in a different {@code Activity}. To accomplish this,
* the {@link io.flutter.embedding.engine.FlutterEngine} will need to be disconnected from the
* surrounding {@code Activity} at an unusual time, preventing this {@code FlutterFragment} from
* correctly managing the relationship between the {@link
* io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}.
*
* <p>Another reason that a developer might choose to manually manage the relationship between
* the {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the
* developer wants to prevent, or explicitly control when the {@link
* io.flutter.embedding.engine.FlutterEngine}'s plugins have access to the surrounding {@code
* Activity}. For example, imagine that this {@code FlutterFragment} only takes up part of the
* screen and the app developer wants to ensure that none of the Flutter plugins are able to
* manipulate the surrounding {@code Activity}. In this case, the developer would not want the
* {@link io.flutter.embedding.engine.FlutterEngine} to have access to the {@code Activity},
* which can be accomplished by setting {@code shouldAttachEngineToActivity} to {@code false}.
*/
@NonNull
public CachedEngineFragmentBuilder shouldAttachEngineToActivity(
boolean shouldAttachEngineToActivity) {
this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically receive {@link
* #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled
* by default.
*
* <p>When enabled, the activity will automatically dispatch back-press events to the fragment's
* {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link
* #onBackPressed()} in client code. If enabled, do <b>not</b> invoke {@link #onBackPressed()}
* manually.
*
* <p>Enabling this behavior relies on explicit behavior in {@link #popSystemNavigator()}. It's
* not recommended to override that method when enabling this attribute, but if you do, you
* should always fall back to calling {@code super.popSystemNavigator()} when not relying on
* custom behavior.
*/
@NonNull
public CachedEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
boolean shouldAutomaticallyHandleOnBackPressed) {
this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
return this;
}
/**
* Whether to delay the Android drawing pass till after the Flutter UI has been displayed.
*
* <p>See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details.
*/
@NonNull
public CachedEngineFragmentBuilder shouldDelayFirstAndroidViewDraw(
@NonNull boolean shouldDelayFirstAndroidViewDraw) {
this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw;
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.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
args.putString(
ARG_FLUTTERVIEW_RENDER_MODE,
renderMode != null ? renderMode.name() : RenderMode.surface.name());
args.putString(
ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name());
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
args.putBoolean(
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw);
return args;
}
/**
* Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on
* properties set on this {@code 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);
}
}
}
/**
* Returns a {@link NewEngineInGroupFragmentBuilder} to create a {@code FlutterFragment} with a
* cached {@link io.flutter.embedding.engine.FlutterEngineGroup} in {@link
* io.flutter.embedding.engine.FlutterEngineGroupCache}.
*
* <p>An {@code IllegalStateException} will be thrown during the lifecycle of the {@code
* FlutterFragment} if a cached {@link io.flutter.embedding.engine.FlutterEngineGroup} is
* requested but does not exist in the {@link
* io.flutter.embedding.engine.FlutterEngineGroupCache}.
*/
@NonNull
public static NewEngineInGroupFragmentBuilder withNewEngineInGroup(
@NonNull String engineGroupId) {
return new NewEngineInGroupFragmentBuilder(engineGroupId);
}
/**
* Builder that creates a new {@code FlutterFragment} that uses a cached {@link
* io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link
* io.flutter.embedding.engine.FlutterEngine} with {@code arguments} that correspond to the values
* set on this {@code Builder}.
*
* <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.NewEngineInGroupFragmentBuilder(MyFlutterFragment.class, engineGroupId)
* .someProperty(...) .someOtherProperty(...) .build<MyFlutterFragment>(); }
*
* <p>Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this
* {@code NewEngineInGroupFragmentBuilder} to add the new properties:
*
* <ol>
* <li>Ensure the {@code FlutterFragment} subclass has a no-arg constructor.
* <li>Subclass this {@code NewEngineInGroupFragmentBuilder}.
* <li>Override the new {@code NewEngineInGroupFragmentBuilder}'s no-arg constructor and invoke
* the super constructor to set the {@code FlutterFragment} subclass: {@code public
* MyBuilder() { super(MyFlutterFragment.class); } }
* <li>Add appropriate property methods for the new properties.
* <li>Override {@link NewEngineInGroupFragmentBuilder#createArgs()}, call through to the super
* method, then add the new properties as arguments in the {@link Bundle}.
* </ol>
*
* Once a {@code NewEngineInGroupFragmentBuilder} subclass is defined, the {@code FlutterFragment}
* subclass can be instantiated as follows. {@code MyFlutterFragment f = new MyBuilder()
* .someExistingProperty(...) .someNewProperty(...) .build<MyFlutterFragment>(); }
*/
public static class NewEngineInGroupFragmentBuilder {
private final Class<? extends FlutterFragment> fragmentClass;
private final String cachedEngineGroupId;
private @NonNull String dartEntrypoint = "main";
private @NonNull String initialRoute = "/";
private @NonNull boolean handleDeeplinking = false;
private @NonNull RenderMode renderMode = RenderMode.surface;
private @NonNull TransparencyMode transparencyMode = TransparencyMode.transparent;
private boolean shouldAttachEngineToActivity = true;
private boolean shouldAutomaticallyHandleOnBackPressed = false;
private boolean shouldDelayFirstAndroidViewDraw = false;
public NewEngineInGroupFragmentBuilder(@NonNull String engineGroupId) {
this(FlutterFragment.class, engineGroupId);
}
public NewEngineInGroupFragmentBuilder(
@NonNull Class<? extends FlutterFragment> fragmentClass, @NonNull String engineGroupId) {
this.fragmentClass = fragmentClass;
this.cachedEngineGroupId = engineGroupId;
}
/** The name of the initial Dart method to invoke, defaults to "main". */
@NonNull
public NewEngineInGroupFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
this.dartEntrypoint = dartEntrypoint;
return this;
}
/**
* The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to
* "/".
*/
@NonNull
public NewEngineInGroupFragmentBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
/**
* Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
* getInitialRoute} returns null.
*/
@NonNull
public NewEngineInGroupFragmentBuilder handleDeeplinking(@NonNull boolean handleDeeplinking) {
this.handleDeeplinking = handleDeeplinking;
return this;
}
/**
* Render Flutter either as a {@link RenderMode#surface} or a {@link 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 NewEngineInGroupFragmentBuilder renderMode(@NonNull RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
/**
* Support a {@link TransparencyMode#transparent} background within {@link
* io.flutter.embedding.android.FlutterView}, or force an {@link TransparencyMode#opaque}
* background.
*
* <p>See {@link TransparencyMode} for implications of this selection.
*/
@NonNull
public NewEngineInGroupFragmentBuilder transparencyMode(
@NonNull TransparencyMode transparencyMode) {
this.transparencyMode = transparencyMode;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically attach its {@code Activity}
* as a control surface for its {@link io.flutter.embedding.engine.FlutterEngine}.
*
* <p>Control surfaces are used to provide Android resources and lifecycle events to plugins
* that are attached to the {@link io.flutter.embedding.engine.FlutterEngine}. If {@code
* shouldAttachEngineToActivity} is true then this {@code FlutterFragment} will connect its
* {@link io.flutter.embedding.engine.FlutterEngine} to the surrounding {@code Activity}, along
* with any plugins that are registered with that {@link FlutterEngine}. This allows plugins to
* access the {@code Activity}, as well as receive {@code Activity}-specific calls, e.g., {@link
* android.app.Activity#onNewIntent(Intent)}. If {@code shouldAttachEngineToActivity} is false,
* then this {@code FlutterFragment} will not automatically manage the connection between its
* {@link io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. The
* {@code Activity} will need to be manually connected to this {@code FlutterFragment}'s {@link
* io.flutter.embedding.engine.FlutterEngine} by the app developer. See {@link
* FlutterEngine#getActivityControlSurface()}.
*
* <p>One reason that a developer might choose to manually manage the relationship between the
* {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the developer
* wants to move the {@link FlutterEngine} somewhere else. For example, a developer might want
* the {@link io.flutter.embedding.engine.FlutterEngine} to outlive the surrounding {@code
* Activity} so that it can be used later in a different {@code Activity}. To accomplish this,
* the {@link io.flutter.embedding.engine.FlutterEngine} will need to be disconnected from the
* surrounding {@code Activity} at an unusual time, preventing this {@code FlutterFragment} from
* correctly managing the relationship between the {@link
* io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}.
*
* <p>Another reason that a developer might choose to manually manage the relationship between
* the {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the
* developer wants to prevent, or explicitly control when the {@link
* io.flutter.embedding.engine.FlutterEngine}'s plugins have access to the surrounding {@code
* Activity}. For example, imagine that this {@code FlutterFragment} only takes up part of the
* screen and the app developer wants to ensure that none of the Flutter plugins are able to
* manipulate the surrounding {@code Activity}. In this case, the developer would not want the
* {@link io.flutter.embedding.engine.FlutterEngine} to have access to the {@code Activity},
* which can be accomplished by setting {@code shouldAttachEngineToActivity} to {@code false}.
*/
@NonNull
public NewEngineInGroupFragmentBuilder shouldAttachEngineToActivity(
boolean shouldAttachEngineToActivity) {
this.shouldAttachEngineToActivity = shouldAttachEngineToActivity;
return this;
}
/**
* Whether or not this {@code FlutterFragment} should automatically receive {@link
* #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled
* by default.
*
* <p>When enabled, the activity will automatically dispatch back-press events to the fragment's
* {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link
* #onBackPressed()} in client code. If enabled, do <b>not</b> invoke {@link #onBackPressed()}
* manually.
*
* <p>This behavior relies on the implementation of {@link #popSystemNavigator()}. It's not
* recommended to override that method when enabling this attribute, but if you do, you should
* always fall back to calling {@code super.popSystemNavigator()} when not relying on custom
* behavior.
*/
@NonNull
public NewEngineInGroupFragmentBuilder shouldAutomaticallyHandleOnBackPressed(
boolean shouldAutomaticallyHandleOnBackPressed) {
this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed;
return this;
}
/**
* Whether to delay the Android drawing pass till after the Flutter UI has been displayed.
*
* <p>See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details.
*/
@NonNull
public NewEngineInGroupFragmentBuilder shouldDelayFirstAndroidViewDraw(
@NonNull boolean shouldDelayFirstAndroidViewDraw) {
this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw;
return this;
}
/**
* Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
*
* <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_GROUP_ID, cachedEngineGroupId);
args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint);
args.putString(ARG_INITIAL_ROUTE, initialRoute);
args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking);
args.putString(
ARG_FLUTTERVIEW_RENDER_MODE,
renderMode != null ? renderMode.name() : RenderMode.surface.name());
args.putString(
ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name());
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
args.putBoolean(
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw);
return args;
}
/**
* Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on
* properties set on this {@code Builder}.
*/
@NonNull
public <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.
@VisibleForTesting @Nullable /* package */ FlutterActivityAndFragmentDelegate delegate;
@NonNull private FlutterActivityAndFragmentDelegate.DelegateFactory delegateFactory = this;
/** Default delegate factory that creates a simple FlutterActivityAndFragmentDelegate instance. */
public FlutterActivityAndFragmentDelegate createDelegate(
FlutterActivityAndFragmentDelegate.Host host) {
return new FlutterActivityAndFragmentDelegate(host);
}
private final OnBackPressedCallback onBackPressedCallback =
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onBackPressed();
}
};
public FlutterFragment() {
// Ensure that we at least have an empty Bundle of arguments so that we don't
// need to continually check for null arguments before grabbing one.
setArguments(new Bundle());
}
/**
* This method exists so that JVM tests can ensure that a delegate exists without putting this
* Fragment through any lifecycle events, because JVM tests cannot handle executing any lifecycle
* methods, at the time of writing this.
*
* <p>The testing infrastructure should be upgraded to make FlutterFragment tests easy to write
* while exercising real lifecycle methods. At such a time, this method should be removed.
*/
// TODO(mattcarroll): remove this when tests allow for it
// (https://github.com/flutter/flutter/issues/43798)
@VisibleForTesting
/* package */ void setDelegateFactory(
@NonNull FlutterActivityAndFragmentDelegate.DelegateFactory delegateFactory) {
this.delegateFactory = delegateFactory;
delegate = delegateFactory.createDelegate(this);
}
/**
* Returns the Android App Component exclusively attached to {@link
* io.flutter.embedding.engine.FlutterEngine}.
*/
@Override
public ExclusiveAppComponent<Activity> getExclusiveAppComponent() {
return delegate;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
delegate = delegateFactory.createDelegate(this);
delegate.onAttach(context);
if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
}
context.registerComponentCallbacks(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
delegate.onRestoreInstanceState(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return delegate.onCreateView(
inflater,
container,
savedInstanceState,
/*flutterViewId=*/ FLUTTER_VIEW_ID,
shouldDelayFirstAndroidViewDraw());
}
@Override
public void onStart() {
super.onStart();
if (stillAttachedForEvent("onStart")) {
delegate.onStart();
}
}
@Override
public void onResume() {
super.onResume();
if (stillAttachedForEvent("onResume")) {
delegate.onResume();
}
}
// TODO(mattcarroll): determine why this can't be in onResume(). Comment reason, or move if
// possible.
@ActivityCallThrough
public void onPostResume() {
if (stillAttachedForEvent("onPostResume")) {
delegate.onPostResume();
}
}
@Override
public void onPause() {
super.onPause();
if (stillAttachedForEvent("onPause")) {
delegate.onPause();
}
}
@Override
public void onStop() {
super.onStop();
if (stillAttachedForEvent("onStop")) {
delegate.onStop();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (stillAttachedForEvent("onDestroyView")) {
delegate.onDestroyView();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (stillAttachedForEvent("onSaveInstanceState")) {
delegate.onSaveInstanceState(outState);
}
}
@Override
public void detachFromFlutterEngine() {
Log.w(
TAG,
"FlutterFragment "
+ this
+ " connection to the engine "
+ getFlutterEngine()
+ " evicted by another attaching activity");
if (delegate != null) {
// Redundant calls are ok.
delegate.onDestroyView();
delegate.onDetach();
}
}
@Override
public void onDetach() {
getContext().unregisterComponentCallbacks(this);
super.onDetach();
if (delegate != null) {
delegate.onDetach();
delegate.release();
delegate = null;
} else {
Log.v(TAG, "FlutterFragment " + this + " onDetach called after release.");
}
}
/**
* The result of a permission request has been received.
*
* <p>See {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])}
*
* <p>
*
* @param requestCode identifier passed with the initial permission request
* @param permissions permissions that were requested
* @param grantResults permission grants or denials
*/
@ActivityCallThrough
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (stillAttachedForEvent("onRequestPermissionsResult")) {
delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
/**
* A new Intent was received by the {@link android.app.Activity} that currently owns this {@link
* Fragment}.
*
* <p>See {@link android.app.Activity#onNewIntent(Intent)}
*
* <p>
*
* @param intent new Intent
*/
@ActivityCallThrough
public void onNewIntent(@NonNull Intent intent) {
if (stillAttachedForEvent("onNewIntent")) {
delegate.onNewIntent(intent);
}
}
/**
* The hardware back button was pressed.
*
* <p>If the fragment uses {@code shouldAutomaticallyHandleOnBackPressed(true)}, this method
* should not be called through. It will be called automatically instead.
*
* <p>See {@link android.app.Activity#onBackPressed()}
*/
@ActivityCallThrough
public void onBackPressed() {
if (stillAttachedForEvent("onBackPressed")) {
delegate.onBackPressed();
}
}
/**
* A result has been returned after an invocation of {@link
* Fragment#startActivityForResult(Intent, int)}.
*
* <p>
*
* @param requestCode request code sent with {@link Fragment#startActivityForResult(Intent, int)}
* @param resultCode code representing the result of the {@code Activity} that was launched
* @param data any corresponding return data, held within an {@code Intent}
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (stillAttachedForEvent("onActivityResult")) {
delegate.onActivityResult(requestCode, resultCode, data);
}
}
/**
* The {@link android.app.Activity} that owns this {@link Fragment} is about to go to the
* background as the result of a user's choice/action, i.e., not as the result of an OS decision.
*
* <p>See {@link android.app.Activity#onUserLeaveHint()}
*/
@ActivityCallThrough
public void onUserLeaveHint() {
if (stillAttachedForEvent("onUserLeaveHint")) {
delegate.onUserLeaveHint();
}
}
/**
* Callback invoked when memory is low.
*
* <p>This implementation forwards a memory pressure warning to the running Flutter app.
*
* <p>
*
* @param level level
*/
@Override
public void onTrimMemory(int level) {
if (stillAttachedForEvent("onTrimMemory")) {
delegate.onTrimMemory(level);
}
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when initializing
* Flutter.
*/
@Override
@NonNull
public FlutterShellArgs getFlutterShellArgs() {
String[] flutterShellArgsArray = getArguments().getStringArray(ARG_FLUTTER_INITIALIZATION_ARGS);
return new FlutterShellArgs(
flutterShellArgsArray != null ? flutterShellArgsArray : new String[] {});
}
/**
* Returns the ID of a statically cached {@link io.flutter.embedding.engine.FlutterEngine} to use
* within this {@code FlutterFragment}, or {@code null} if this {@code FlutterFragment} does not
* want to use a cached {@link io.flutter.embedding.engine.FlutterEngine}.
*/
@Nullable
@Override
public String getCachedEngineId() {
return getArguments().getString(ARG_CACHED_ENGINE_ID, null);
}
/**
* Returns the ID of a statically cached {@link io.flutter.embedding.engine.FlutterEngineGroup} to
* use within this {@code FlutterFragment}, or {@code null} if this {@code FlutterFragment} does
* not want to use a cached {@link io.flutter.embedding.engine.FlutterEngineGroup}.
*/
@Override
@Nullable
public String getCachedEngineGroupId() {
return getArguments().getString(ARG_CACHED_ENGINE_GROUP_ID, null);
}
/**
* Returns true a {@code FlutterEngine} was explicitly created and injected into the {@code
* FlutterFragment} rather than one that was created implicitly in the {@code FlutterFragment}.
*/
/* package */ boolean isFlutterEngineInjected() {
return delegate.isFlutterEngineFromHost();
}
/**
* Returns false if the {@link io.flutter.embedding.engine.FlutterEngine} within this {@code
* FlutterFragment} should outlive the {@code FlutterFragment}, itself.
*
* <p>Defaults to true if no custom {@link io.flutter.embedding.engine.FlutterEngine is provided},
* false if a custom {@link FlutterEngine} is provided.
*/
@Override
public boolean shouldDestroyEngineWithHost() {
boolean explicitDestructionRequested =
getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, false);
if (getCachedEngineId() != null || delegate.isFlutterEngineFromHost()) {
// Only destroy a cached engine if explicitly requested by app developer.
return explicitDestructionRequested;
} else {
// If this Fragment created the FlutterEngine, destroy it by default unless
// explicitly requested not to.
return getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
}
}
/**
* Returns the name of the Dart method that this {@code FlutterFragment} should execute to start a
* Flutter app.
*
* <p>Defaults to "main".
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public String getDartEntrypointFunctionName() {
return getArguments().getString(ARG_DART_ENTRYPOINT, "main");
}
/**
* The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint function.
*
* <p>A value of null means do not pass any arguments to Dart's entrypoint function.
*
* <p>Subclasses may override this method to directly control the Dart entrypoint arguments.
*/
@Override
@Nullable
public List<String> getDartEntrypointArgs() {
return getArguments().getStringArrayList(ARG_DART_ENTRYPOINT_ARGS);
}
/**
* Returns the library URI of the Dart method that this {@code FlutterFragment} should execute to
* start a Flutter app.
*
* <p>Defaults to null (example value: "package:foo/bar.dart").
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@Nullable
public String getDartEntrypointLibraryUri() {
return getArguments().getString(ARG_DART_ENTRYPOINT_URI);
}
/**
* A custom path to the bundle that contains this Flutter app's resources, e.g., Dart code
* snapshots.
*
* <p>When unspecified, the value is null, which defaults to the app bundle path defined in {@link
* io.flutter.embedding.engine.loader.FlutterLoader#findAppBundlePath()}.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public String getAppBundlePath() {
return getArguments().getString(ARG_APP_BUNDLE_PATH);
}
/**
* Returns the initial route that should be rendered within Flutter, once the Flutter app starts.
*
* <p>Defaults to {@code null}, which signifies a route of "/" in Flutter.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@Nullable
public String getInitialRoute() {
return getArguments().getString(ARG_INITIAL_ROUTE);
}
/**
* Returns the desired {@link RenderMode} for the {@link io.flutter.embedding.android.FlutterView}
* displayed in this {@code FlutterFragment}.
*
* <p>Defaults to {@link RenderMode#surface}.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public RenderMode getRenderMode() {
String renderModeName =
getArguments().getString(ARG_FLUTTERVIEW_RENDER_MODE, RenderMode.surface.name());
return RenderMode.valueOf(renderModeName);
}
/**
* Returns the desired {@link TransparencyMode} for the {@link
* io.flutter.embedding.android.FlutterView} displayed in this {@code FlutterFragment}.
*
* <p>Defaults to {@link TransparencyMode#transparent}.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public TransparencyMode getTransparencyMode() {
String transparencyModeName =
getArguments()
.getString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.transparent.name());
return TransparencyMode.valueOf(transparencyModeName);
}
@Override
@Nullable
public SplashScreen provideSplashScreen() {
FragmentActivity parentActivity = getActivity();
if (parentActivity instanceof SplashScreenProvider) {
SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity;
return splashScreenProvider.provideSplashScreen();
}
return null;
}
/**
* Hook for subclasses to return a {@link io.flutter.embedding.engine.FlutterEngine} with whatever
* configuration is desired.
*
* <p>By default this method defers to this {@code FlutterFragment}'s surrounding {@code
* Activity}, if that {@code Activity} implements {@link
* io.flutter.embedding.android.FlutterEngineProvider}. If this method is overridden, the
* surrounding {@code Activity} will no longer be given an opportunity to provide a {@link
* io.flutter.embedding.engine.FlutterEngine}, unless the subclass explicitly implements that
* behavior.
*
* <p>Consider returning a cached {@link io.flutter.embedding.engine.FlutterEngine} instance from
* this method to avoid the typical warm-up time that a new {@link
* io.flutter.embedding.engine.FlutterEngine} instance requires.
*
* <p>If null is returned then a new default {@link io.flutter.embedding.engine.FlutterEngine}
* will be created to back this {@code FlutterFragment}.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@Nullable
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
// Defer to the FragmentActivity that owns us to see if it wants to provide a
// FlutterEngine.
FlutterEngine flutterEngine = null;
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterEngineProvider) {
// Defer to the Activity that owns us to provide a FlutterEngine.
Log.v(TAG, "Deferring to attached Activity to provide a FlutterEngine.");
FlutterEngineProvider flutterEngineProvider = (FlutterEngineProvider) attachedActivity;
flutterEngine = flutterEngineProvider.provideFlutterEngine(getContext());
}
return flutterEngine;
}
/**
* Hook for subclasses to obtain a reference to the {@link
* io.flutter.embedding.engine.FlutterEngine} that is owned by this {@code FlutterActivity}.
*/
@Nullable
public FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
}
@Nullable
@Override
public PlatformPlugin providePlatformPlugin(
@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
if (activity != null) {
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel(), this);
} else {
return null;
}
}
/**
* Configures a {@link io.flutter.embedding.engine.FlutterEngine} after its creation.
*
* <p>This method is called after {@link #provideFlutterEngine(Context)}, and after the given
* {@link io.flutter.embedding.engine.FlutterEngine} has been attached to the owning {@code
* FragmentActivity}. See {@link
* io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(
* ExclusiveAppComponent, Lifecycle)}.
*
* <p>It is possible that the owning {@code FragmentActivity} opted not to connect itself as an
* {@link io.flutter.embedding.engine.plugins.activity.ActivityControlSurface}. In that case, any
* configuration, e.g., plugins, must not expect or depend upon an available {@code Activity} at
* the time that this method is invoked.
*
* <p>The default behavior of this method is to defer to the owning {@code FragmentActivity} as a
* {@link io.flutter.embedding.android.FlutterEngineConfigurator}. Subclasses can override this
* method if the subclass needs to override the {@code FragmentActivity}'s behavior, or add to it.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterEngineConfigurator) {
((FlutterEngineConfigurator) attachedActivity).configureFlutterEngine(flutterEngine);
}
}
/**
* Hook for the host to cleanup references that were established in {@link
* #configureFlutterEngine(FlutterEngine)} before the host is destroyed or detached.
*
* <p>This method is called in {@link #onDetach()}.
*/
@Override
public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterEngineConfigurator) {
((FlutterEngineConfigurator) attachedActivity).cleanUpFlutterEngine(flutterEngine);
}
}
/**
* See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and {@link
* CachedEngineFragmentBuilder#shouldAttachEngineToActivity()}.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate}
*/
@Override
public boolean shouldAttachEngineToActivity() {
return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY);
}
/**
* Whether to handle the deeplinking from the {@code Intent} automatically if the {@code
* getInitialRoute} returns null.
*/
@Override
public boolean shouldHandleDeeplinking() {
return getArguments().getBoolean(ARG_HANDLE_DEEPLINKING);
}
@Override
public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) {
// Hook for subclasses.
}
@Override
public void onFlutterTextureViewCreated(@NonNull FlutterTextureView flutterTextureView) {
// Hook for subclasses.
}
/**
* Invoked after the {@link io.flutter.embedding.android.FlutterView} within this {@code
* FlutterFragment} starts rendering pixels to the screen.
*
* <p>This method forwards {@code onFlutterUiDisplayed()} to its attached {@code Activity}, if the
* attached {@code Activity} implements {@link FlutterUiDisplayListener}.
*
* <p>Subclasses that override this method must call through to the {@code super} method.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
public void onFlutterUiDisplayed() {
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterUiDisplayListener) {
((FlutterUiDisplayListener) attachedActivity).onFlutterUiDisplayed();
}
}
/**
* Invoked after the {@link io.flutter.embedding.android.FlutterView} within this {@code
* FlutterFragment} stops rendering pixels to the screen.
*
* <p>This method forwards {@code onFlutterUiNoLongerDisplayed()} to its attached {@code
* Activity}, if the attached {@code Activity} implements {@link FlutterUiDisplayListener}.
*
* <p>Subclasses that override this method must call through to the {@code super} method.
*
* <p>Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
public void onFlutterUiNoLongerDisplayed() {
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterUiDisplayListener) {
((FlutterUiDisplayListener) attachedActivity).onFlutterUiNoLongerDisplayed();
}
}
@Override
public boolean shouldRestoreAndSaveState() {
if (getArguments().containsKey(ARG_ENABLE_STATE_RESTORATION)) {
return getArguments().getBoolean(ARG_ENABLE_STATE_RESTORATION);
}
if (getCachedEngineId() != null) {
return false;
}
return true;
}
@Override
public void updateSystemUiOverlays() {
if (delegate != null) {
delegate.updateSystemUiOverlays();
}
}
/**
* Give the host application a chance to take control of the app lifecycle events.
*
* <p>Return {@code false} means the host application dispatches these app lifecycle events, while
* return {@code true} means the engine dispatches these events.
*
* <p>Defaults to {@code true}.
*/
@Override
public boolean shouldDispatchAppLifecycleState() {
return true;
}
/**
* {@inheritDoc}
*
* <p>Avoid overriding this method when using {@code
* shouldAutomaticallyHandleOnBackPressed(true)}. If you do, you must always {@code return
* super.popSystemNavigator()} rather than {@code return false}. Otherwise the navigation behavior
* will recurse infinitely between this method and {@link #onBackPressed()}, breaking navigation.
*/
@Override
public boolean popSystemNavigator() {
if (getArguments().getBoolean(ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, false)) {
FragmentActivity activity = getActivity();
if (activity != null) {
// Unless we disable the callback, the dispatcher call will trigger it. This will then
// trigger the fragment's onBackPressed() implementation, which will call through to the
// dart side and likely call back through to this method, creating an infinite call loop.
onBackPressedCallback.setEnabled(false);
activity.getOnBackPressedDispatcher().onBackPressed();
onBackPressedCallback.setEnabled(true);
return true;
}
}
// Hook for subclass. No-op if returns false.
return false;
}
@VisibleForTesting
@NonNull
boolean shouldDelayFirstAndroidViewDraw() {
return getArguments().getBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW);
}
private boolean stillAttachedForEvent(String event) {
if (delegate == null) {
Log.w(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release.");
return false;
}
if (!delegate.isAttached()) {
Log.w(TAG, "FlutterFragment " + hashCode() + " " + event + " called after detach.");
return false;
}
return true;
}
/**
* Annotates methods in {@code FlutterFragment} that must be called by the containing {@code
* Activity}.
*/
@interface ActivityCallThrough {}
}