在 Android 应用中添加 Flutter Fragment

Add Flutter Fragment Header

本指南介绍如何在现有 Android 应用中添加 Flutter Fragment。在 Android 中,Fragment 表示较大 UI 的模块化部分。Fragment 可用于显示滑动抽屉、标签页内容、ViewPager 中的页面,或者它可能仅仅表示单 Activity 应用中的普通屏幕。Flutter 提供 FlutterFragment,以便开发者可以在他们可以使用普通 Fragment 的任何地方呈现 Flutter 体验。

如果 Activity 同样适用于你的应用需求,请考虑 使用 FlutterActivity 而不是 FlutterFragment,它使用起来更快捷、更简单。

FlutterFragment 允许开发者控制 Fragment 中 Flutter 体验的以下详细信息

  • 初始 Flutter 路由
  • 要执行的 Dart 入口点
  • 不透明与半透明背景
  • FlutterFragment 是否应控制其周围的 Activity
  • 是否应使用新的 FlutterEngine 或缓存的 FlutterEngine

FlutterFragment 还附带了一些必须从其周围的 Activity 转发的调用。这些调用允许 Flutter 对操作系统事件做出适当的反应。

本指南介绍了 FlutterFragment 的所有类型及其要求。

使用新的 FlutterEngineFlutterFragment 添加到 Activity

使用 FlutterFragment 的第一步是将其添加到主机 Activity

要将 FlutterFragment 添加到主机 Activity,请在 Activity 中的 onCreate() 中实例化并附加 FlutterFragment 的实例,或在适合您应用的其他时间执行此操作

MyActivity.java
public class MyActivity extends FragmentActivity {
    // Define a tag String to represent the FlutterFragment within this
    // Activity's FragmentManager. This value can be whatever you'd like.
    private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";

    // Declare a local variable to reference the FlutterFragment so that you
    // can forward calls to it later.
    private FlutterFragment flutterFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Inflate a layout that has a container for your FlutterFragment.
        // For this example, assume that a FrameLayout exists with an ID of
        // R.id.fragment_container.
        setContentView(R.layout.my_activity_layout);

        // Get a reference to the Activity's FragmentManager to add a new
        // FlutterFragment, or find an existing one.
        FragmentManager fragmentManager = getSupportFragmentManager();

        // Attempt to find an existing FlutterFragment,
        // in case this is not the first time that onCreate() was run.
        flutterFragment = (FlutterFragment) fragmentManager
            .findFragmentByTag(TAG_FLUTTER_FRAGMENT);

        // Create and attach a FlutterFragment if one does not exist.
        if (flutterFragment == null) {
            flutterFragment = FlutterFragment.createDefault();

            fragmentManager
                .beginTransaction()
                .add(
                    R.id.fragment_container,
                    flutterFragment,
                    TAG_FLUTTER_FRAGMENT
                )
                .commit();
        }
    }
}
MyActivity.kt
class MyActivity : FragmentActivity() {
  companion object {
    // Define a tag String to represent the FlutterFragment within this
    // Activity's FragmentManager. This value can be whatever you'd like.
    private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
  }

  // Declare a local variable to reference the FlutterFragment so that you
  // can forward calls to it later.
  private var flutterFragment: FlutterFragment? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Inflate a layout that has a container for your FlutterFragment. For
    // this example, assume that a FrameLayout exists with an ID of
    // R.id.fragment_container.
    setContentView(R.layout.my_activity_layout)

    // Get a reference to the Activity's FragmentManager to add a new
    // FlutterFragment, or find an existing one.
    val fragmentManager: FragmentManager = supportFragmentManager

    // Attempt to find an existing FlutterFragment, in case this is not the
    // first time that onCreate() was run.
    flutterFragment = fragmentManager
      .findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?

    // Create and attach a FlutterFragment if one does not exist.
    if (flutterFragment == null) {
      var newFlutterFragment = FlutterFragment.createDefault()
      flutterFragment = newFlutterFragment
      fragmentManager
        .beginTransaction()
        .add(
          R.id.fragment_container,
          newFlutterFragment,
          TAG_FLUTTER_FRAGMENT
        )
        .commit()
    }
  }
}

前面的代码足以呈现一个 Flutter UI,该 UI 以对您的 main() Dart 入口点、初始 Flutter 路由 / 和新的 FlutterEngine 的调用开始。但是,此代码不足以实现所有预期的 Flutter 行为。Flutter 依赖于必须从您的主机 Activity 转发到 FlutterFragment 的各种操作系统信号。以下示例显示了这些调用

MyActivity.java
public class MyActivity extends FragmentActivity {
    @Override
    public void onPostResume() {
        super.onPostResume();
        flutterFragment.onPostResume();
    }

    @Override
    protected void onNewIntent(@NonNull Intent intent) {
        flutterFragment.onNewIntent(intent);
    }

    @Override
    public void onBackPressed() {
        flutterFragment.onBackPressed();
    }

    @Override
    public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults
    ) {
        flutterFragment.onRequestPermissionsResult(
            requestCode,
            permissions,
            grantResults
        );
    }

    @Override
    public void onActivityResult(
        int requestCode,
        int resultCode,
        @Nullable Intent data
    ) {
        super.onActivityResult(requestCode, resultCode, data);
        flutterFragment.onActivityResult(
            requestCode,
            resultCode,
            data
        );
    }

    @Override
    public void onUserLeaveHint() {
        flutterFragment.onUserLeaveHint();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        flutterFragment.onTrimMemory(level);
    }
}
MyActivity.kt
class MyActivity : FragmentActivity() {
  override fun onPostResume() {
    super.onPostResume()
    flutterFragment!!.onPostResume()
  }

  override fun onNewIntent(@NonNull intent: Intent) {
    flutterFragment!!.onNewIntent(intent)
  }

  override fun onBackPressed() {
    flutterFragment!!.onBackPressed()
  }

  override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String?>,
    grantResults: IntArray
  ) {
    flutterFragment!!.onRequestPermissionsResult(
      requestCode,
      permissions,
      grantResults
    )
  }

  override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    data: Intent?
  ) {
    super.onActivityResult(requestCode, resultCode, data)
    flutterFragment!!.onActivityResult(
      requestCode,
      resultCode,
      data
    )
  }

  override fun onUserLeaveHint() {
    flutterFragment!!.onUserLeaveHint()
  }

  override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    flutterFragment!!.onTrimMemory(level)
  }
}

将操作系统信号转发到 Flutter 后,您的 FlutterFragment 将按预期工作。您现在已将 FlutterFragment 添加到现有的 Android 应用中。

最简单的集成路径使用新的 FlutterEngine,该引擎具有不平凡的初始化时间,导致在 Flutter 初始化并首次呈现之前出现空白 UI。通过使用缓存的预热 FlutterEngine,可以避免大部分此类时间开销,下面将对此进行讨论。

使用预热的 FlutterEngine

默认情况下,FlutterFragment 会创建其自己的 FlutterEngine 实例,而这需要较长的预热时间。这意味着您的用户会在短时间内看到一个空白的 Fragment。您可以通过使用已存在的预热 FlutterEngine 实例来缓解大部分预热时间。

要在 FlutterFragment 中使用预热的 FlutterEngine,请使用 withCachedEngine() 工厂方法实例化 FlutterFragment

MyApplication.java
// Somewhere in your app, before your FlutterFragment is needed,
// like in the Application class ...
// Instantiate a FlutterEngine.
FlutterEngine flutterEngine = new FlutterEngine(context);

// Start executing Dart code in the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
);

// Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
FlutterEngineCache
  .getInstance()
  .put("my_engine_id", flutterEngine);
MyActivity.java
FlutterFragment.withCachedEngine("my_engine_id").build();
MyApplication.kt
// Somewhere in your app, before your FlutterFragment is needed,
// like in the Application class ...
// Instantiate a FlutterEngine.
val flutterEngine = FlutterEngine(context)

// Start executing Dart code in the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
)

// Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
FlutterEngineCache
  .getInstance()
  .put("my_engine_id", flutterEngine)
MyActivity.java
FlutterFragment.withCachedEngine("my_engine_id").build()

FlutterFragment 在内部了解 FlutterEngineCache,并基于提供给 withCachedEngine() 的 ID 检索预热的 FlutterEngine

通过提供预热的 FlutterEngine(如前所示),您的应用可以尽可能快地渲染第一个 Flutter 帧。

使用缓存引擎的初始路由

在使用新 FlutterEngine 配置 FlutterActivityFlutterFragment 时,可以使用初始路由的概念。但是,在使用缓存引擎时,FlutterActivityFlutterFragment 不提供初始路由的概念。这是因为缓存引擎预计已经运行 Dart 代码,这意味着配置初始路由为时已晚。

希望其缓存引擎以自定义初始路由开始的开发者可以在执行 Dart 入口点之前配置其缓存 FlutterEngine 以使用自定义初始路由。以下示例演示了如何将初始路由与缓存引擎一起使用

MyApplication.java
public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // Instantiate a FlutterEngine.
    flutterEngine = new FlutterEngine(this);
    // Configure an initial route.
    flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}
MyApplication.kt
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine
  override fun onCreate() {
    super.onCreate()
    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)
    // Configure an initial route.
    flutterEngine.navigationChannel.setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}

通过设置导航通道的初始路由,关联的 FlutterEnginerunApp() Dart 函数的初始执行时显示所需的路由。

runApp() 初始执行后更改导航通道的初始路由属性无效。希望在不同的 ActivityFragment 之间使用相同的 FlutterEngine 并切换这些显示之间的路由的开发者需要设置一个方法通道并明确指示其 Dart 代码更改 Navigator 路由。

显示启动画面

Flutter 内容的初始显示需要一些等待时间,即使使用了预热 FlutterEngine 也是如此。为了帮助改善这段短暂等待期的用户体验,Flutter 支持在 Flutter 渲染其第一帧之前显示启动画面(也称为“启动屏幕”)。有关如何显示启动屏幕的说明,请参阅 启动屏幕指南

使用指定初始路由运行 Flutter

一个 Android 应用可能包含许多独立的 Flutter 体验,在不同的 FlutterFragment 中运行,使用不同的 FlutterEngine。在这些场景中,每个 Flutter 体验通常从不同的初始路由(除 / 之外的路由)开始。为了实现这一点,FlutterFragmentBuilder 允许您指定所需的初始路由,如下所示

MyActivity.java
// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build();
MyActivity.kt
// With a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build()

从指定入口点运行 Flutter

与不同的初始路由类似,不同的 FlutterFragment 可能希望执行不同的 Dart 入口点。在典型的 Flutter 应用中,只有一个 Dart 入口点:main(),但您可以定义其他入口点。

FlutterFragment 支持指定要为给定 Flutter 体验执行的所需的 Dart 入口点。要指定入口点,请构建 FlutterFragment,如下所示

MyActivity.java
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build();
MyActivity.kt
val flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build()

FlutterFragment 配置导致执行名为 mySpecialEntrypoint() 的 Dart 入口点。请注意,括号 () 不包含在 dartEntrypoint String 名称中。

控制 FlutterFragment 的渲染模式

FlutterFragment 可以使用 SurfaceView 渲染其 Flutter 内容,或者使用 TextureView。默认值为 SurfaceView,其性能明显优于 TextureView。但是,SurfaceView 无法交错在 Android View 层级的中间。SurfaceView 必须是层级中的最底层 View 或最顶层 View。此外,在 Android N 之前的 Android 版本中,SurfaceView 无法动画,因为它们的布局和渲染与 View 层级的其余部分不同步。如果这些用例中的任何一个都是你的应用的要求,那么你需要使用 TextureView 而不是 SurfaceView。通过使用具有 texture RenderModeFlutterFragment 来选择 TextureView

MyActivity.java
// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build();

// With a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build();
MyActivity.kt
// With a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build()

// With a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build()

使用所示的配置,生成的 FlutterFragment 将其 UI 渲染到 TextureView

显示具有透明度的 FlutterFragment

默认情况下,FlutterFragment 使用 SurfaceView 渲染具有不透明背景。(请参阅“控制 FlutterFragment 的渲染模式”。)对于未由 Flutter 绘制的任何像素,该背景为黑色。出于性能原因,使用不透明背景渲染是首选的渲染模式。在 Android 上使用透明度渲染 Flutter 会对性能产生负面影响。但是,有很多设计需要 Flutter 体验中的透明像素,这些像素会显示在底层 Android UI 中。出于这个原因,Flutter 支持 FlutterFragment 中的半透明度。

要为 FlutterFragment 启用透明度,请使用以下配置构建它

MyActivity.java
// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();
MyActivity.kt
// Using a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()

// Using a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()

FlutterFragment 及其 Activity 之间的关系

一些应用选择将 Fragment 用作整个 Android 屏幕。在这些应用中,Fragment 控制系统装饰(如 Android 的状态栏、导航栏和方向)是合理的。

Fullscreen Flutter

在其他应用中,Fragment 仅用于表示 UI 的一部分。FlutterFragment 可用于实现抽屉内部、视频播放器或单个卡片。在这些情况下,FlutterFragment 影响 Android 的系统装饰是不合适的,因为同一 Window 中还有其他 UI 部分。

Flutter as Partial UI

FlutterFragment 带有一个概念,有助于区分以下情况:FlutterFragment 应该能够控制其宿主 Activity 的情况,以及 FlutterFragment 仅应该影响其自身行为的情况。要防止 FlutterFragment 向 Flutter 插件公开其 Activity,并防止 Flutter 控制 Activity 的系统 UI,请在 FlutterFragmentBuilder 中使用 shouldAttachEngineToActivity() 方法,如下所示

MyActivity.java
// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build();
MyActivity.kt
// Using a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build()

// Using a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build()

false 传递给 shouldAttachEngineToActivity() Builder 方法可防止 Flutter 与周围的 Activity 交互。默认值为 true,这允许 Flutter 和 Flutter 插件与周围的 Activity 交互。