跳到主内容

将 Flutter Fragment 添加到 Android 应用

了解如何将 Flutter Fragment 添加到现有的 Android 应用中。

Add Flutter Fragment Header

本指南介绍了如何将 Flutter Fragment 添加到现有的 Android 应用中。在 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,请在 ActivityonCreate() 方法中(或在适合您应用的任何其他时间)实例化并附加一个 FlutterFragment 实例。

MyActivity.kt
kotlin
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()
    }
  }
}
MyActivity.java
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();
        }
    }
}

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

MyActivity.kt
kotlin
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)
  }
}
MyActivity.java
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);
    }
}

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

最简单的集成路径是使用新的 FlutterEngine,它需要相当长的初始化时间,导致在 Flutter 初始化并首次渲染之前出现空白 UI。使用缓存的、预热的 FlutterEngine 可以避免大部分时间开销,这一点将在接下来讨论。

使用预热(pre-warmed)的 FlutterEngine

#

默认情况下,FlutterFragment 会创建自己的 FlutterEngine 实例,这需要相当长的预热时间。这意味着用户会短暂地看到一个空白的 Fragment。通过使用现有的、预热的 FlutterEngine 实例,可以减轻大部分预热时间。

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

MyApplication.kt
kotlin
// 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
kotlin
FlutterFragment.withCachedEngine("my_engine_id").build()
MyApplication.java
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
java
FlutterFragment.withCachedEngine("my_engine_id").build();

FlutterFragment 在内部知道 FlutterEngineCache,并根据提供给 withCachedEngine() 的 ID 检索预热的 FlutterEngine

通过提供如上所示的预热 FlutterEngine,您的应用可以尽快渲染出第一帧 Flutter 内容。

使用缓存引擎时的初始路由

#

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

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

MyApplication.kt
kotlin
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)
  }
}
MyApplication.java
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);
  }
}

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

在首次执行 runApp() 后更改导航通道的初始路由属性不会产生任何效果。希望在不同 ActivityFragment 之间使用同一个 FlutterEngine 并在这些显示之间切换路由的开发者,需要设置一个方法通道(Method Channel),并显式指示其 Dart 代码更改 Navigator 路由。

显示闪屏(Splash Screen)

#

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

以指定的初始路由运行 Flutter

#

一个 Android 应用可能包含许多独立的 Flutter 体验,在不同的 FlutterFragment 中运行,并使用不同的 FlutterEngine。在这种情况下,每个 Flutter 体验通常以不同的初始路由(非 / 的路由)开始。为了方便起见,FlutterFragmentBuilder 允许您指定所需的初始路由,如下所示:

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

从指定的入口点运行 Flutter

#

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

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

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

FlutterFragment 配置将执行名为 mySpecialEntrypoint() 的 Dart 入口点。请注意,dartEntrypoint 字符串名称中不包含括号 ()

控制 FlutterFragment 的渲染模式

#

FlutterFragment 可以使用 SurfaceView 来渲染其 Flutter 内容,也可以使用 TextureView。默认是 SurfaceView,其性能明显优于 TextureView。但是,SurfaceView 不能穿插在 Android View 层级结构中。SurfaceView 必须是层级结构中最底层的 View 或最顶层的 View。此外,在 Android N 之前的 Android 版本中,SurfaceView 无法进行动画处理,因为它们的布局和渲染与其余 View 层级结构不同步。如果这些用例中的任何一个是您的应用需求,则需要使用 TextureView 代替 SurfaceView。通过使用 texture RenderMode 构建 FlutterFragment 来选择 TextureView

MyActivity.kt
kotlin
// 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()
MyActivity.java
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();

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

显示透明的 FlutterFragment

#

默认情况下,FlutterFragment 使用 SurfaceView 渲染不透明背景。(参见“控制 FlutterFragment 的渲染模式”。)对于任何未由 Flutter 绘制的像素,该背景为黑色。出于性能考虑,使用不透明背景是首选的渲染模式。Flutter 在 Android 上以透明度进行渲染会对性能产生负面影响。然而,许多设计需要在 Flutter 体验中使用透明像素,以便透出底层的 Android UI。因此,Flutter 支持 FlutterFragment 中的半透明度。

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

MyActivity.kt
kotlin
// 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()
MyActivity.java
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();

FlutterFragment 与其 Activity 之间的关系

#

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

Fullscreen Flutter

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

Flutter as Partial UI

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

MyActivity.kt
kotlin
// 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()
MyActivity.java
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();

false 传递给 shouldAttachEngineToActivity() 构建器方法会阻止 Flutter 与周围的 Activity 交互。默认值为 true,这允许 Flutter 和 Flutter 插件与周围的 Activity 交互。