跳至主要內容

將 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 適當地對 OS 事件做出反應。

本指南中將說明所有種類的 FlutterFragment 及其需求。

使用新的 FlutterEngineFlutterFragment 新增至 Activity

#

使用 FlutterFragment 的第一件事是將其新增至主機 Activity

若要將 FlutterFragment 新增至主機 Activity,請在 Activity 內的 onCreate() 中或在適合您應用程式的其他時間,建立並附加 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();
        }
    }
}

先前的程式碼足以呈現從呼叫您的 main() Dart 進入點、/ 的初始 Flutter 路由和新的 FlutterEngine 開始的 Flutter UI。但是,此程式碼不足以實現所有預期的 Flutter 行為。Flutter 依賴必須從您的主機 Activity 轉發到 FlutterFragment 的各種 OS 訊號。以下範例中顯示了這些呼叫

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

將 OS 訊號轉發到 Flutter 後,您的 FlutterFragment 會如預期般運作。您現在已將 FlutterFragment 新增至現有的 Android 應用程式。

最簡單的整合路徑是使用新的 FlutterEngine,它具有非平凡的初始化時間,導致在 Flutter 初始化並首次呈現之前出現空白 UI。透過使用快取的預熱 FlutterEngine,可以避免大部分這種時間負擔,接下來將討論這一點。

使用預先暖機的 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 並在這些顯示器之間切換路由的開發人員,需要設定方法通道並明確指示其 Dart 程式碼變更 Navigator 路由。

顯示啟動畫面

#

即使使用預熱的 FlutterEngine,Flutter 內容的初始顯示也需要一些等待時間。為了協助改善此短暫等待期間的使用者體驗,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 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
// 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 未繪製的任何像素,該背景為黑色。基於效能考量,使用不透明背景進行呈現是偏好的渲染模式。在 Android 上使用透明度呈現 Flutter 會對效能產生負面影響。但是,有許多設計需要在 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 控制系統外觀(例如 Android 的狀態列、導覽列和方向)是合理的。

Fullscreen Flutter

在其他應用程式中,Fragment 僅用於表示 UI 的一部分。FlutterFragment 可能用於實作抽屜的內部、影片播放器或單張卡片。在這些情況下,FlutterFragment 影響 Android 的系統外觀是不恰當的,因為在同一個 Window 中還有其他 UI 元件。

Flutter as Partial UI

FlutterFragment 提供一個概念,有助於區分 FlutterFragment 應該能夠控制其宿主 Activity 的情況,以及 FlutterFragment 僅應影響自身行為的情況。為了防止 FlutterFragment 將其 Activity 暴露給 Flutter 外掛程式,並防止 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() Builder 方法可防止 Flutter 與周圍的 Activity 互動。預設值為 true,允許 Flutter 和 Flutter 外掛程式與周圍的 Activity 互動。