Skip to main content

向 Android 应用添加 Flutter Fragment

添加Flutter Fragment标题

本指南介绍如何将 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 及其要求。

向使用新 FlutterEngineActivity 添加 FlutterFragment

#

使用 FlutterFragment 的第一步是将其添加到宿主 Activity 中。

要将 FlutterFragment 添加到宿主 Activity,请在 ActivityonCreate() 中实例化并附加 FlutterFragment 实例,或者在对您的应用有效的其他时间进行:

MyActivity.kt
kotlin
class MyActivity : FragmentActivity() {
  companion object {
    // 定义一个 tag 字符串,用于在该 Activity 的 FragmentManager 中表示 FlutterFragment。此值可以是您想要的任何值。
    private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
  }

  // 声明一个局部变量来引用 FlutterFragment,以便您可以稍后向其转发调用。
  private var flutterFragment: FlutterFragment? = null

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

    // 加载一个包含 FlutterFragment 容器的布局。对于此示例,假设存在一个 ID 为 R.id.fragment_container 的 FrameLayout。
    setContentView(R.layout.my_activity_layout)

    // 获取对 Activity 的 FragmentManager 的引用,以添加新的 FlutterFragment 或查找现有的 FlutterFragment。
    val fragmentManager: FragmentManager = supportFragmentManager

    // 尝试查找现有的 FlutterFragment,以防这不是 onCreate() 第一次运行。
    flutterFragment = fragmentManager
      .findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?

    // 如果不存在 FlutterFragment,则创建并附加一个。
    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 {
    // 定义一个 tag 字符串,用于在该 Activity 的 FragmentManager 中表示 FlutterFragment。此值可以是您想要的任何值。
    private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";

    // 声明一个局部变量来引用 FlutterFragment,以便您可以稍后向其转发调用。
    private FlutterFragment flutterFragment;

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

        // 加载一个包含 FlutterFragment 容器的布局。对于此示例,假设存在一个 ID 为 R.id.fragment_container 的 FrameLayout。
        setContentView(R.layout.my_activity_layout);

        // 获取对 Activity 的 FragmentManager 的引用,以添加新的 FlutterFragment 或查找现有的 FlutterFragment。
        FragmentManager fragmentManager = getSupportFragmentManager();

        // 尝试查找现有的 FlutterFragment,以防这不是 onCreate() 第一次运行。
        flutterFragment = (FlutterFragment) fragmentManager
            .findFragmentByTag(TAG_FLUTTER_FRAGMENT);

        // 如果不存在 FlutterFragment,则创建并附加一个。
        if (flutterFragment == null) {
            flutterFragment = FlutterFragment.createDefault();

            fragmentManager
                .beginTransaction()
                .add(
                    R.id.fragment_container,
                    flutterFragment,
                    TAG_FLUTTER_FRAGMENT
                )
                .commit();
        }
    }
}

之前的代码足以呈现从调用您的 main() Dart 入口点开始的 Flutter UI,初始 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(将在后面讨论),可以避免大部分此时间开销。

使用预热的 FlutterEngine

#

默认情况下,FlutterFragment 会创建它自己的 FlutterEngine 实例,这需要非平凡的预热时间。这意味着您的用户会短暂看到一个空白的 Fragment。您可以通过使用现有的预热 FlutterEngine 实例来减少大部分这种预热时间。

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

MyApplication.kt
kotlin
// 在您的应用程序中的某个地方,在需要 FlutterFragment 之前,例如在 Application 类中...
// 实例化一个 FlutterEngine。
val flutterEngine = FlutterEngine(context)

// 开始在 FlutterEngine 中执行 Dart 代码。
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
)

// 缓存预热的 FlutterEngine,以便稍后由 FlutterFragment 使用。
FlutterEngineCache
  .getInstance()
  .put("my_engine_id", flutterEngine)
MyActivity.java
kotlin
FlutterFragment.withCachedEngine("my_engine_id").build()
MyApplication.java
java
// 在您的应用程序中的某个地方,在需要 FlutterFragment 之前,例如在 Application 类中...
// 实例化一个 FlutterEngine。
FlutterEngine flutterEngine = new FlutterEngine(context);

// 开始在 FlutterEngine 中执行 Dart 代码。
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
);

// 缓存预热的 FlutterEngine,以便稍后由 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()
    // 实例化一个 FlutterEngine。
    flutterEngine = FlutterEngine(this)
    // 配置初始路由。
    flutterEngine.navigationChannel.setInitialRoute("your/route/here");
    // 开始执行 Dart 代码以预热 FlutterEngine。
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )
    // 缓存 FlutterEngine 以供 FlutterActivity 或 FlutterFragment 使用。
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}
MyApplication.java
java
public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // 实例化一个 FlutterEngine。
    flutterEngine = new FlutterEngine(this);
    // 配置初始路由。
    flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
    // 开始执行 Dart 代码以预热 FlutterEngine。
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );
    // 缓存 FlutterEngine 以供 FlutterActivity 或 FlutterFragment 使用。
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}

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

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

显示启动画面

#

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

使用指定的初始路由运行 Flutter

#

Android 应用可能包含许多独立的 Flutter 体验,它们在不同的 FlutterFragment 中运行,并使用不同的 FlutterEngine。在这些情况下,每个 Flutter 体验通常从不同的初始路由(/ 以外的路由)开始是很常见的。为了促进这一点,FlutterFragmentBuilder 允许您指定所需的初始路由,如下所示:

MyActivity.kt
kotlin
// 使用新的 FlutterEngine。
val flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build()
MyActivity.java
java
// 使用新的 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 String 名称中。

控制 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
// 使用新的 FlutterEngine。
val flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build()

// 使用缓存的 FlutterEngine。
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build()
MyActivity.java
java
// 使用新的 FlutterEngine。
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build();

// 使用缓存的 FlutterEngine。
FlutterFragment 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.kt
kotlin
// 使用新的 FlutterEngine。
val flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()

// 使用缓存的 FlutterEngine。
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()
MyActivity.java
java
// 使用新的 FlutterEngine。
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();

// 使用缓存的 FlutterEngine。
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();

FlutterFragment 及其 Activity 之间的关系

#

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

全屏 Flutter

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

Flutter 作为部分 UI

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

MyActivity.kt
kotlin
// 使用新的 FlutterEngine。
val flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build()

// 使用缓存的 FlutterEngine。
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build()
MyActivity.java
java
// 使用新的 FlutterEngine。
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build();

// 使用缓存的 FlutterEngine。
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build();

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