在 Android 应用中添加 Flutter 页面

本指南讲述了如何在一个现有的 Android 应用中添加单个 Flutter 页面。添加到应用中的单个 Flutter 页面可以是不透明的普通页面,也可以是透明的页面。这两种页面的使用都会在本指南中提到。

添加一个普通的 Flutter 页面

#

Add Flutter Screen Header

步骤 1:在 AndroidManifest.xml 中添加 FlutterActivity

#

Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内:

xml
<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

上述代码中的 @style/LaunchTheme 可以替换为想要在你的 FlutterActivity 中使用的其他 Android 主题。主题的选择决定 Android 系统展示框架所使用的颜色,例如 Android 的导航栏,以及 Flutter UI 自身的第一次渲染前 FlutterActivity 的背景色。

步骤 2:加载 FlutterActivity

#

在你的清单文件中注册了 FlutterActivity 之后,根据需要,你可以在应用中的任意位置添加打开 FlutterActivity 的代码。下边的代码展示了如何在 OnClickListener 的点击事件中打开 FlutterActivity

ExistingActivity.kt
kotlin
MyButton(onClick = {
    startActivity(
        FlutterActivity.createDefaultIntent(this)
    )
})

@Composable
fun MyButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Launch Flutter!")
    }
}
ExistingActivity.kt
kotlin
myButton.setOnClickListener {
  startActivity(
    FlutterActivity.createDefaultIntent(this)
  )
}
ExistingActivity.java
java
myButton.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity.createDefaultIntent(currentActivity)
    );
  }
});

上述的例子假定了你的 Dart 代码入口是调用 main(),并且你的 Flutter 初始路由是 '/'。 Dart 代码入口不能通过 Intent 改变,但是初始路由可以通过 Intent 来修改。下面的例子讲解了如何打开一个自定义 Flutter 初始路由的 FlutterActivity

ExistingActivity.kt
kotlin
MyButton(onClick = {
  startActivity(
    FlutterActivity
      .withNewEngine()
      .initialRoute("/my_route")
      .build(this)
  )
})

@Composable
fun MyButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Launch Flutter!")
    }
}
ExistingActivity.kt
kotlin
myButton.setOnClickListener {
  startActivity(
    FlutterActivity
      .withNewEngine()
      .initialRoute("/my_route")
      .build(this)
  )
}
ExistingActivity.java
java
myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withNewEngine()
        .initialRoute("/my_route")
        .build(currentActivity)
      );
  }
});

可以用你想要的初始路由替换掉 "/my_route"

工厂方法 withNewEngine() 可以用于配置一个 FlutterActivity,它会在内部创建一个属于自己的 FlutterEngine 实例,这会有一个明显的初始化时间。另外一种可选的做法是让 FlutterActivity 使用一个预热且缓存的 FlutterEngine,这可以最小化 Flutter 初始化的时间。这种方式接下来会讨论到。

步骤 3:(可选)使用缓存的 FlutterEngine

#

每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine

要预热一个 FlutterEngine,可以在你的应用中找一个合理的地方实例化一个 FlutterEngine。下面的这个例子是在 Application 类中预先初始化一个 FlutterEngine

MyApplication.kt
kotlin
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine

  override fun onCreate() {
    super.onCreate()

    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )

    // Cache the FlutterEngine to be used by FlutterActivity.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}
MyApplication.java
java
public class MyApplication extends Application {
  public FlutterEngine flutterEngine;
  
  @Override
  public void onCreate() {
    super.onCreate();
    // Instantiate a FlutterEngine.
    flutterEngine = new FlutterEngine(this);

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );

    // Cache the FlutterEngine to be used by FlutterActivity.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}

传给 FlutterEngineCache 的 ID 可以是你想要的任何名称。确保 FlutterActivityFlutterFragment 在使用缓存的 FlutterEngine 时,传递了同样的 ID。基于缓存的 FlutterEngine 来使用 FlutterActivity 会在后续讨论到。

要使用预热且缓存的 FlutterEngine 时,让你的 FlutterActivity 从缓存中获取 FlutterEngine,而不是创建一个新的。可以使用 FlutterActivitywithCachedEngine() 方法来实现:

ExistingActivity.kt
kotlin
myButton.setOnClickListener {
  startActivity(
    FlutterActivity
      .withCachedEngine("my_engine_id")
      .build(this)
  )
}
ExistingActivity.java
java
myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withCachedEngine("my_engine_id")
        .build(currentActivity)
      );
  }
});

当使用 withCachedEngine() 方法时,传递你缓存对应 FlutterEngine 时用的相同 ID。

现在,当你加载 FlutterActivity 时,在展示 Flutter 内容前的延迟会明显降低。

为缓存的 FlutterEngine 设置初始路由

#

The concept of an initial route is available when configuring a FlutterActivity or a FlutterFragment with a new FlutterEngine. However, FlutterActivity and FlutterFragment don't offer the concept of an initial route when using a cached engine. This is because a cached engine is expected to already be running Dart code, which means it's too late to configure the initial route.

Developers that would like their cached engine to begin with a custom initial route can configure their cached FlutterEngine to use a custom initial route just before executing the Dart entrypoint. The following example demonstrates the use of an initial route with a cached engine:

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);
  }
}

By setting the initial route of the navigation channel, the associated FlutterEngine displays the desired route upon initial execution of the runApp() Dart function.

Changing the initial route property of the navigation channel after the initial execution of runApp() has no effect. Developers who would like to use the same FlutterEngine between different Activitys and Fragments and switch the route between those displays need to set up a method channel and explicitly instruct their Dart code to change Navigator routes.

添加一个透明主题的 FlutterActivity

#

Add Flutter Screen With Translucency Header

大部分的全屏 Flutter 交互页面是不透明的。但是,一些应用可能会发布一个类似模态框的 Flutter 页面,例如,一个对话框或者底部工作表。 Flutter 默认支持透明的 FlutterActivity

要将你的 FlutterActivity 设置为透明,在创建和加载 FlutterActivity 的常规步骤中做如下的变更。

步骤 1:使用透明的主题

#

Android 需要一个特殊的主题属性来让 Activity 以一个透明的背景渲染。使用如下属性来创建或者修改一个 Android 主题:

xml
<style name="MyTheme" parent="@style/MyParentTheme">
  <item name="android:windowIsTranslucent">true</item>
</style>

然后,将透明主题应用到你的 FlutterActivity

xml
<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/MyTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

现在你的 FlutterActivity 已经支持透明化。下一步,你需要在打开 FlutterActivity 时显式启用透明模式。

步骤 2:启动透明的 FlutterActivity

#

要打开透明背景的 FlutterActivity,需要把对应的 BackgroundMode 传递给 IntentBuilder

ExistingActivity.kt
kotlin
// Using a new FlutterEngine.
startActivity(
  FlutterActivity
    .withNewEngine()
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(this)
);

// Using a cached FlutterEngine.
startActivity(
  FlutterActivity
    .withCachedEngine("my_engine_id")
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(this)
);
ExistingActivity.java
java
// Using a new FlutterEngine.
startActivity(
  FlutterActivity
    .withNewEngine()
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);

// Using a cached FlutterEngine.
startActivity(
  FlutterActivity
    .withCachedEngine("my_engine_id")
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);

现在你的 FlutterAcivity 的背景已经是透明的了。