向 Android 應用中新增 Flutter Fragment

Add Flutter Fragment Header

本篇指南介紹如何向一個現有的 Android 應用中新增 Flutter Fragment。在 Android 開發中,一個 Fragment 代表了一塊較大的模組化 UI。 Fragment 可能被用來展示滑動抽屜、標籤內容和 ViewPager 中的頁面,或者在單 Activity 應用中,Fragment 可能僅代表正常的螢幕內容。 Flutter 提供了FlutterFragment,以便於開發者們可以在任何使用常規 Fragment 的地方呈現 Flutter 的內容。

如果 Activity 同樣適用於您的應用需求,可以考慮 使用 FlutterActivity 而非 FlutterFragment,前者更加快捷易用。

FlutterFragment 允許開發者在 Fragment 中控制以下 Flutter 的開發細節:

  • Flutter 初始路由

  • 將要執行的 Dart 入口

  • 非透明或者透明的背景

  • FlutterFragment 是否能控制它外層的 Activity

  • 使用新的還是快取的 FlutterEngine

FlutterFragment 還提供了一些回呼(Callback)事件,這些回呼(Callback)必須由它所在的 Activity 觸發執行。這些回呼(Callback)允許 Flutter 適時地響應一些系統事件。

這篇指南介紹了 FlutterFragment 的所有使用方式和使用要求。

使用新的 FlutterEngineActivity 中新增 FlutterFragment

使用 FlutterFragment 的第一步是將其新增進宿主 Activity

要向宿主 Activity 中新增 FlutterFragment,需要在 ActivityonCreate() 或者其它合適的地方,例項化 FlutterFragment 並且與 Activity 繫結。

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

上面的程式碼會以 main() 為 Dart 入口函式, / 為初始路由,並使用新的 FlutterEngine,能夠正確渲染出 Flutter UI。但是,這些程式碼還無法使 Flutter 如預期一樣完全正常地工作。 Flutter 依賴作業系統的各種訊號,這些訊號必須透過宿主 Activity 傳送到 FlutterFragment 中。下面的範例展示了這些系統回呼(Callback):

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 onUserLeaveHint() {
        flutterFragment.onUserLeaveHint();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        flutterFragment.onTrimMemory(level);
    }
}
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 onUserLeaveHint() {
    flutterFragment!!.onUserLeaveHint()
  }

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

隨著 OS 訊號傳遞到 Flutter,您的 FlutterFragment 可以如預期正常工作。現在可以嘗試將 FlutterFragment 新增進您的 Android 應用了。

使用新的 FlutterEngine 是最簡單的整合方式,但是會存在一段明顯的初始化時間,此時,在 Flutter 初始化和首次渲染完成之前會出現短暫的白屏。使用快取、預熱的 FlutterEngine 則可以避免上述的大部分耗時,下面我們將討論這些內容。

使用預熱的 FlutterEngine

預設情況下,FlutterFragment 會建立它自己的 FlutterEngine 例項,同時也需要不少的啟動時間。這就意味著您的使用者會看到短暫的白屏。透過使用已存在的、預熱的 FlutterEngine 就可以大幅度減少啟動的耗時。

要在 FlutterFragment 中使用預熱 FlutterEngine,可以使用工廠方法 withCachedEngine() 例項化 FlutterFragment

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

FlutterFragment 內部可存取 FlutterEngineCache,並且可以根據傳遞給 withCachedEngine() 的 ID 獲取預熱的 FlutterEngine

如上所示,透過提供預熱的 FlutterEngine,您的應用將以最快速度渲染出第一幀。

快取引擎中的初始路由

當配置一個使用新 FlutterEngineFlutterActivity 或者 FlutterFragment 時,會使用到初始路由的概念。但是,使用快取中的 Flutter 引擎時, FlutterActivity 或者 FlutterFragment 則沒有涉及初始路由的概念。這是因為被快取的引擎理論上已經執行了 Dart 程式碼,在這時配置初始路由已經太遲了。

開發者如果想要讓快取中的引擎從自訂的初始路由開始執行,那麼可以執行 Dart 入口前,為快取的 FlutterEngine 配置自訂的初始路由。如下面這個例子:

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);
  }
}
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() 方法首次執行後,展示已配置的路由頁面。

runApp() 的首次執行之後,修改導航通道中的初始路由屬性是不會生效的。想要在不同的 ActivityFragment 之間使用同一個 FlutterEngine,並且在其展示時切換不同的路由,開發者需要設定一個方法通道,來明確地通知他們的 Dart 程式碼切換 Navigator 路由。

展示閃屏頁

即使使用了預熱的 FlutterEngine,第一次展示 Flutter 的內容仍然需要一些時間。為了更進一步提升使用者體驗,Flutter 支援在第一幀渲染完成之前展示閃屏頁。關於如何展示閃屏頁的詳細說明,請參閱這篇 閃屏頁指南

指定 Flutter 執行的初始路由

一個 Android 應用中可能包含很多獨立的 Flutter 介面,這些介面顯示在不同的 FlutterFragment 上,每個 FlutterFragmentFlutterEngine 也是獨立的。在這些情況下,每個 Flutter 介面透過不同的初始路由(除 / 以外的路由)啟動是很正常的。為此,FlutterFragmentBuilder 允許指定一個您希望的初始路由,如下所示:

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

指定 Flutter 執行的入口

和變化的初始路由類似,不同的 FlutterFragment 可能需要執行不同的 Dart 程式碼入口。正常的 Flutter 應用中,只會有一個 main() 入口,但是您也可以定義不同的入口。

FlutterFragment 支援指定需要的 Dart 入口以執行對應的 Flutter 介面。下面的程式碼展示瞭如何在建構 FlutterFragment 時指定一個入口。

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

這裡,FlutterFragment 的配置會將 Dart 入口的執行函式設定為 mySpecialEntrypoint()。需要注意的是,括號 () 不包含在 dartEntrypointString 型別的引數中。

控制 FlutterFragment 的渲染模式

FlutterFragment 可以選擇使用 SurfaceView 或者 TextureView 來渲染其內容。預設配置的 SurfaceView 在效能上明顯好於 TextureView。然而,SurfaceView 無法插入到 Android 的 View 層級之中。 SurfaceView 在檢視層級中必須是最底層的 View 或者最最上層的 View。此外,在 Android N 之前,SurfaceView 無法用於製作動畫,因為它們的佈局和渲染無法和檢視層級中的其它 View 同步。如果上述這些使用案例之一在您的應用需求之中,您需要使用 TextureView 替換 SurfaceView。要選擇 TextureView,可以在建構 FlutterFragment 時指定 RenderModetexture

// 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();
// 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 繪製的畫素在背景中都是黑色的。出於效能方面的考慮,我們優先選擇使用不透明的背景進行渲染。渲染透明的 Flutter 介面在 Android 平臺上會產生效能方面的負面影響。但是許多設計都需要 Flutter 介面中包含透明的畫素以顯示底層的 Android UI。因此,Flutter 支援 FlutterFragment 半透明。

要啟動一個透明的 FlutterFragment,可以使用以下方式進行建構:

// 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();
// 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 將其 Activity 暴露給 Flutter 外掛,以免 Flutter 控制 Activity 的系統 UI,可以使用 FlutterFragmentBuilder 中的 shouldAttachEngineToActivity() 方法。如下所示:

// 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();
// 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()

傳遞 falseBuildershouldAttachEngineToActivity() 方法,可防止 Flutter 與所屬的 Activity 互動。預設值為 true,此時允許 Flutter 和 Flutter 外掛與 Activity 互動。