將 Flutter Fragment 加入 Android 應用程式

本指南說明如何將 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 及其需求。
以新的 FlutterEngine 將 FlutterFragment 加入 Activity
#要使用 FlutterFragment,首先需將其加入主機 Activity。
若要將 FlutterFragment 加入主機 Activity,請在 Activity 的 onCreate() 中,或在其他適合你應用的時機,實例化並附加 FlutterFragment 的實例:
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()
}
}
}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 進入點(entrypoint)開始,初始 Flutter 路由為 /,並建立一個新的 FlutterEngine。然而,這段程式碼尚不足以實現所有預期的 Flutter 行為。Flutter 依賴於各種作業系統(OS)訊號,這些訊號必須從你的主機 Activity 傳遞到 FlutterFragment。下列範例展示了這些呼叫:
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)
}
}</string?>
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。 大部分的時間延遲可以透過使用 快取且預先啟動(pre-warmed)的 FlutterEngine 來避免, 相關內容將在下節說明。
使用預先啟動的 FlutterEngine
#預設情況下,FlutterFragment 會自行建立一個 FlutterEngine 實例,這需要一定的啟動時間。 這表示使用者會在短暫時間內看到空白的 Fragment。 你可以透過使用已存在且預先啟動的 FlutterEngine 實例, 來減少大部分的啟動延遲。
若要在 FlutterFragment 中使用預先啟動的 FlutterEngine, 請使用 withCachedEngine() 工廠方法實例化 FlutterFragment。
// 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()// 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();FlutterFragment 會在內部知曉 FlutterEngineCache, 並根據提供給 withCachedEngine() 的 ID 取得預先加載(pre-warmed)的 FlutterEngine。
如前所示,透過提供預先加載的 FlutterEngine, 您的應用程式能夠盡可能快速地渲染出 第一個 Flutter 畫面。
使用快取引擎(cached engine)指定初始路由
#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:
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)
}
}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.
顯示啟動畫面(Splash Screen)
#即使使用了預先加載的 FlutterEngine, 在最初顯示 Flutter 內容時仍需等待一段時間。 為了提升用戶在這段短暫等待期間的體驗,Flutter 支援顯示啟動畫面(也稱為「啟動畫面」或「launch screen」),直到 Flutter 渲染出第一個畫面為止。關於如何顯示啟動畫面的詳細說明,請參閱 splash screen guide。
以指定的初始路由運行 Flutter
#一個 Android 應用程式可能包含多個獨立的 Flutter 體驗, 分別運行於不同的 FlutterFragment,並搭配不同的 FlutterEngine。在這些情境下, 每個 Flutter 體驗通常會以不同的初始路由(而非 /)開始。 為了實現這一點,FlutterFragment 的 Builder 允許您指定所需的初始路由,如下所示:
// With a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("myInitialRoute/")
.build()// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("myInitialRoute/")
.build();從指定的 entrypoint 執行 Flutter
#與變更 initial route 類似,不同的 FlutterFragment 可能希望執行不同的 Dart entrypoint。在典型的 Flutter 應用程式中,通常只有一個 Dart entrypoint:main(),但你也可以定義其他 entrypoint。
FlutterFragment 支援指定要為特定 Flutter 體驗執行的 Dart entrypoint。 若要指定 entrypoint,請如以下方式建立 FlutterFragment:
val flutterFragment = FlutterFragment.withNewEngine()
.dartEntrypoint("mySpecialEntrypoint")
.build()FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.dartEntrypoint("mySpecialEntrypoint")
.build();FlutterFragment 設定會執行名為 mySpecialEntrypoint() 的 Dart entrypoint。 請注意,() 括號不包含在 dartEntrypoint String 名稱中。
控制 FlutterFragment 的渲染模式
#FlutterFragment 可以使用 SurfaceView 來渲染其 Flutter 內容,或是使用 TextureView。 預設值為 SurfaceView,其效能明顯優於 TextureView。然而,SurfaceView 無法插入在 Android View 階層的中間。 SurfaceView 必須是階層中最底層的 View,或是階層中最頂層的 View。 此外,在 Android N 之前的版本上,SurfaceView 無法進行動畫,因為它們的版面配置與渲染無法與其他 View 階層同步。 如果您的應用程式有上述任一需求,則必須使用 TextureView 取代 SurfaceView。 可透過建構帶有 texture RenderMode 的 FlutterFragment 來選擇 TextureView:
// 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()// 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啟用透明度,請使用以下設定進行建構:
// 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()// 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 控制系統 chrome(如 Android 的狀態列、導覽列和螢幕方向)是合理的。

在其他應用中,Fragment 只用來呈現 UI 的某一部分。FlutterFragment 可能被用來實作抽屜內部、影片播放器或單一卡片。在這些情境下,讓 FlutterFragment 影響 Android 的系統 chrome 就不太合適,因為同一個 Window 內還有其他 UI 元素。

FlutterFragment 提供了一個概念,協助區分何時應讓 FlutterFragment 能夠控制其所屬的 Activity,以及何時 FlutterFragment 只應影響自身行為。為了避免 FlutterFragment 將其 Activity 暴露給 Flutter 外掛,並防止 Flutter 控制 Activity 的系統 UI,請在 FlutterFragment 的 Builder 中使用 shouldAttachEngineToActivity() 方法,如下所示:
// 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()// 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 互動。