向 Android 应用中添加 Flutter Fragment

Add Flutter Fragment Header

This guide describes how to add a Flutter Fragment to an existing Android app.In Android, a Fragment represents a modular piece of a larger UI. AFragment might be used to present a sliding drawer, tabbed content, a page ina ViewPager, or it might simply represent a normal screen in asingle-Activity app. Flutter provides a FlutterFragment so that developerscan present a Flutter experience any place that they can use a regularFragment.

If an Activity is equally applicable for your application needs, considerusing a FlutterActivity instead of a FlutterFragment, which is quicker andeasier to use.

FlutterFragment allows developers to control the following details of theFlutter experience within the Fragment:

  • Initial Flutter route.
  • Dart entrypoint to execute.
  • Opaque vs translucent background.
  • Whether FlutterFragment should control its surrounding Activity.
  • Whether a new FlutterEngine or a cached FlutterEngine should be used.

FlutterFragment also comes with a number of calls that must be forwarded fromits surrounding Activity. These calls allow Flutter to react appropriately toOS events.

All varieties of FlutterFragment, and its requirements, are described in thisguide.

Add a FlutterFragment to an Activity with a new FlutterEngine

The first thing to do to use a FlutterFragment is to add it to a hostActivity.

To add a FlutterFragment to a host Activity, instantiate andattach an instance of FlutterFragment in onCreate() within theActivity, or at another time that works for your app:

MyActivity.java

  1. public class MyActivity extends FragmentActivity {
  2. // Define a tag String to represent the FlutterFragment within this
  3. // Activity's FragmentManager. This value can be whatever you'd like.
  4. private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
  5.  
  6. // Declare a local variable to reference the FlutterFragment so that you
  7. // can forward calls to it later.
  8. private FlutterFragment flutterFragment;
  9.  
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13.  
  14. // Inflate a layout that has a container for your FlutterFragment. For
  15. // this example, assume that a FrameLayout exists with an ID of
  16. // R.id.fragment_container.
  17. setContentView(R.layout.my_activity_layout);
  18.  
  19. // Get a reference to the Activity's FragmentManager to add a new
  20. // FlutterFragment, or find an existing one.
  21. FragmentManager fragmentManager = getSupportFragmentManager();
  22.  
  23. // Attempt to find an existing FlutterFragment, in case this is not the
  24. // first time that onCreate() was run.
  25. flutterFragment = (FlutterFragment) fragmentManager
  26. .findFragmentByTag(TAG_FLUTTER_FRAGMENT);
  27.  
  28. // Create and attach a FlutterFragment if one does not exist.
  29. if (flutterFragment == null) {
  30. flutterFragment = FlutterFragment.createDefault();
  31.  
  32. fragmentManager
  33. .beginTransaction()
  34. .add(
  35. R.id.fragment_container,
  36. flutterFragment,
  37. TAG_FLUTTER_FRAGMENT
  38. )
  39. .commit();
  40. }
  41. }
  42. }

MyActivity.kt

  1. class MyActivity : FragmentActivity() {
  2. companion object {
  3. // Define a tag String to represent the FlutterFragment within this
  4. // Activity's FragmentManager. This value can be whatever you'd like.
  5. private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
  6. }
  7.  
  8. // Declare a local variable to reference the FlutterFragment so that you
  9. // can forward calls to it later.
  10. private var flutterFragment: FlutterFragment? = null
  11.  
  12. override fun onCreate(savedInstanceState: Bundle?) {
  13. super.onCreate(savedInstanceState)
  14.  
  15. // Inflate a layout that has a container for your FlutterFragment. For
  16. // this example, assume that a FrameLayout exists with an ID of
  17. // R.id.fragment_container.
  18. setContentView(R.layout.my_activity_layout)
  19.  
  20. // Get a reference to the Activity's FragmentManager to add a new
  21. // FlutterFragment, or find an existing one.
  22. val fragmentManager: FragmentManager = supportFragmentManager
  23.  
  24. // Attempt to find an existing FlutterFragment, in case this is not the
  25. // first time that onCreate() was run.
  26. flutterFragment = fragmentManager
  27. .findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?
  28.  
  29. // Create and attach a FlutterFragment if one does not exist.
  30. if (flutterFragment == null) {
  31. var newFlutterFragment = FlutterFragment.createDefault()
  32. flutterFragment = newFlutterFragment
  33. fragmentManager
  34. .beginTransaction()
  35. .add(
  36. R.id.fragment_container,
  37. newFlutterFragment,
  38. TAG_FLUTTER_FRAGMENT
  39. )
  40. .commit()
  41. }
  42. }
  43. }

The previous code is sufficient to render a Flutter UI that begins with a call toyour main() Dart entrypoint, an initial Flutter route of /, and a newFlutterEngine. However, this code is not sufficient to achieve all expectedFlutter behavior. Flutter depends on various OS signals that must beforwarded from your host Activity to FlutterFragment. These calls are shown in the following example:

MyActivity.java

  1. public class MyActivity extends FragmentActivity {
  2. @Override
  3. public void onPostResume() {
  4. super.onPostResume();
  5. flutterFragment.onPostResume();
  6. }
  7.  
  8. @Override
  9. protected void onNewIntent(@NonNull Intent intent) {
  10. flutterFragment.onNewIntent(intent);
  11. }
  12.  
  13. @Override
  14. public void onBackPressed() {
  15. flutterFragment.onBackPressed();
  16. }
  17.  
  18. @Override
  19. public void onRequestPermissionsResult(
  20. int requestCode,
  21. @NonNull String[] permissions,
  22. @NonNull int[] grantResults
  23. ) {
  24. flutterFragment.onRequestPermissionsResult(
  25. requestCode,
  26. permissions,
  27. grantResults
  28. );
  29. }
  30.  
  31. @Override
  32. public void onUserLeaveHint() {
  33. flutterFragment.onUserLeaveHint();
  34. }
  35.  
  36. @Override
  37. public void onTrimMemory(int level) {
  38. super.onTrimMemory(level);
  39. flutterFragment.onTrimMemory(level);
  40. }
  41. }

MyActivity.kt

  1. class MyActivity : FragmentActivity() {
  2. override fun onPostResume() {
  3. super.onPostResume()
  4. flutterFragment!!.onPostResume()
  5. }
  6.  
  7. override fun onNewIntent(@NonNull intent: Intent) {
  8. flutterFragment!!.onNewIntent(intent)
  9. }
  10.  
  11. override fun onBackPressed() {
  12. flutterFragment!!.onBackPressed()
  13. }
  14.  
  15. override fun onRequestPermissionsResult(
  16. requestCode: Int,
  17. permissions: Array<String?>,
  18. grantResults: IntArray
  19. ) {
  20. flutterFragment!!.onRequestPermissionsResult(
  21. requestCode,
  22. permissions,
  23. grantResults
  24. )
  25. }
  26.  
  27. override fun onUserLeaveHint() {
  28. flutterFragment!!.onUserLeaveHint()
  29. }
  30.  
  31. override fun onTrimMemory(level: Int) {
  32. super.onTrimMemory(level)
  33. flutterFragment!!.onTrimMemory(level)
  34. }
  35. }

With the OS signals forwarded to Flutter, your FlutterFragment works asexpected. You have now added a FlutterFragment to your existing Android app.

The simplest integration path uses a new FlutterEngine, which comes with anon-trivial initialization time, leading to a blank UI until Flutter isinitialized and rendered the first time. Most of this time overhead can beavoided by using a cached, pre-warmed FlutterEngine, which is discussed next.

Using a pre-warmed FlutterEngine

By default, a FlutterFragment creates its own instance of a FlutterEngine,which requires non-trivial warm-up time. This means your user sees a blankFragment for a brief moment. You can mitigate most of this warm-up time byusing an existing, pre-warmed instance of FlutterEngine.

To use a pre-warmed FlutterEngine in a FlutterFragment, instantiate aFlutterFragment with the withCachedEngine() factory method.

MyApplication.java

  1. // Somewhere in your app, before your FlutterFragment is needed, like in the
  2. // Application class ...
  3. // Instantiate a FlutterEngine.
  4. FlutterEngine flutterEngine = new FlutterEngine(context);
  5.  
  6. // Start executing Dart code in the FlutterEngine.
  7. flutterEngine.getDartExecutor().executeDartEntrypoint(
  8. DartEntrypoint.createDefault()
  9. );
  10.  
  11. // Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
  12. FlutterEngineCache
  13. .getInstance()
  14. .put("my_engine_id", flutterEngine);

MyActivity.java

  1. flutterFragment.withCachedEngine("my_engine_id").build();

MyApplication.kt

  1. // Somewhere in your app, before your FlutterFragment is needed, like in the
  2. // Application class ...
  3. // Instantiate a FlutterEngine.
  4. val flutterEngine = FlutterEngine(context)
  5.  
  6. // Start executing Dart code in the FlutterEngine.
  7. flutterEngine.getDartExecutor().executeDartEntrypoint(
  8. DartEntrypoint.createDefault()
  9. )
  10.  
  11. // Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
  12. FlutterEngineCache
  13. .getInstance()
  14. .put("my_engine_id", flutterEngine)

MyActivity.kt

  1. flutterFragment.withCachedEngine("my_engine_id").build()

FlutterFragment internally knows about FlutterEngineCache and retrieves thepre-warmed FlutterEngine based on the ID given to withCachedEngine().

By providing a pre-warmed FlutterEngine, as previously shown, your app renders thefirst Flutter frame as quickly as possible.

Display a splash screen

The initial display of Flutter content requires some wait time, even if apre-warmed FlutterEngine is used. To help improve the user experience aroundthis brief waiting period, Flutter supports the display of a splash screen untilFlutter renders its first frame. For instructions about how to show a splash screen,see the Android splash screen guide.

Run Flutter with a specified initial route

An Android app might contain many independent Flutter experiences, running indifferent FlutterFragments, with different FlutterEngines. In thesescenarios, it’s common for each Flutter experience to begin with differentinitial routes (routes other than /). To facilitate this, FlutterFragment’sBuilder allows you to specify a desired initial route, as shown:

MyActivity.java

  1. // With a new FlutterEngine.
  2. FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
  3. .initialRoute("myInitialRoute/")
  4. .build();

MyActivity.kt

  1. // With a new FlutterEngine.
  2. val flutterFragment = FlutterFragment.withNewEngine()
  3. .initialRoute("myInitialRoute/")
  4. .build()

备忘 FlutterFragment’s initial route property has no effect when a pre-warmed FlutterEngine is used because the pre-warmed FlutterEngine already chose an initial route. The initial route can be chosen explicitly when pre-warming a FlutterEngine.

Run Flutter from a specified entrypoint

Similar to varying initial routes, different FlutterFragments may want toexecute different Dart entrypoints. In a typical Flutter app, there is only oneDart entrypoint: main(), but you can define other entrypoints.

FlutterFragment supports specification of the desired Dart entrypoint toexecute for the given Flutter experience. To specify an entrypoint, buildFlutterFragment, as shown:

MyActivity.java

  1. FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
  2. .dartEntrypoint("mySpecialEntrypoint")
  3. .build();

MyActivity.kt

  1. val flutterFragment = FlutterFragment.withNewEngine()
  2. .dartEntrypoint("mySpecialEntrypoint")
  3. .build()

The FlutterFragment configuration results in the execution of a Dartentrypoint called mySpecialEntrypoint(). Notice that the parentheses () arenot included in the dartEntrypoint String name.

备忘 FlutterFragment’s Dart entrypoint property has no effect when a pre-warmed FlutterEngine is used because the pre-warmed FlutterEngine already executed a Dart entrypoint. The Dart entrypoint can be chosen explicitly when pre-warming a FlutterEngine.

Control FlutterFragment’s render mode

FlutterFragment can either use a SurfaceView to render its Flutter content,or it can use a TextureView. The default is SurfaceView, which issignificantly better for performance than TextureView. However, SurfaceViewcan’t be interleaved in the middle of an Android View hierarchy. ASurfaceView must either be the bottommost View in the hierarchy, or thetopmost View in the hierarchy. Additionally, on Android versions beforeAndroid N, SurfaceViews can’t be animated becuase their layout and renderingaren’t synchronized with the rest of the View hierarchy. If either of theseuse cases are requirements for your app, then you need to use TextureView insteadof SurfaceView. Select a TextureView by building a FlutterFragment with atexture RenderMode:

MyActivity.java

  1. // With a new FlutterEngine.
  2. FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
  3. .renderMode(FlutterView.RenderMode.texture)
  4. .build();
  5.  
  6. // With a cached FlutterEngine.
  7. FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .renderMode(FlutterView.RenderMode.texture)
  9. .build();

MyActivity.kt

  1. // With a new FlutterEngine.
  2. val flutterFragment = FlutterFragment.withNewEngine()
  3. .renderMode(FlutterView.RenderMode.texture)
  4. .build()
  5.  
  6. // With a cached FlutterEngine.
  7. val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .renderMode(FlutterView.RenderMode.texture)
  9. .build()

Using the configuration shown, the resulting FlutterFragment renders its UI toa TextureView.

Display a FlutterFragment with transparency

By default, FlutterFragment renders with an opaque background, using aSurfaceView. (See “Control FlutterFragment’s rendermode.”) That background is black for any pixels that aren’t painted by Flutter.Rendering with an opaque background is the preferred rendering mode forperformance reasons. Flutter rendering with transparency on Android negativelyaffects performance. However, there are many designs that require transparentpixels in the Flutter experience that show through to the underlying Android UI.For this reason, Flutter supports translucency in a FlutterFragment.

备忘 Both SurfaceView and TextureView support transparency. However, when a SurfaceView is instructed to render with transparency, it positions itself at a higher z-index than all other Android Views, which means it appears above all other Views. This is a limitation of SurfaceView. If it’s acceptable to render your Flutter experience on top of all other content, then FlutterFragment’s default RenderMode of surface is the RenderMode that you should use. However, if you need to display Android Views both above and below your Flutter experience, then you must specify a RenderMode of texture. See “Control FlutterFragment’s render mode” for information about controlling the RenderMode.

To enable transparency for a FlutterFragment, build it with the followingconfiguration:

MyActivity.java

  1. // Using a new FlutterEngine.
  2. FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
  3. .transparencyMode(FlutterView.TransparencyMode.transparent)
  4. .build();
  5.  
  6. // Using a cached FlutterEngine.
  7. FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .transparencyMode(FlutterView.TransparencyMode.transparent)
  9. .build();

MyActivity.kt

  1. // Using a new FlutterEngine.
  2. val flutterFragment = FlutterFragment.withNewEngine()
  3. .transparencyMode(FlutterView.TransparencyMode.transparent)
  4. .build()
  5.  
  6. // Using a cached FlutterEngine.
  7. val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .transparencyMode(FlutterView.TransparencyMode.transparent)
  9. .build()

The relationship beween FlutterFragment and its Activity

Some apps choose to use Fragments as entire Android screens. In these apps, itwould be reasonable for a Fragment to control system chrome like Android’sstatus bar, navigation bar, and orientation.

Fullscreen Flutter

In other apps, Fragments are used to represent only a portion of a UI. AFlutterFragment might be used to implement the inside of a drawer, a videoplayer, or a single card. In these situations, it would be inappropriate for theFlutterFragment to affect Android’s system chrome because there are other UIpieces within the same Window.

Flutter as Partial UI

FlutterFragment comes with a concept that helps differentiate between the casewhen a FlutterFragment should be able to control its host Activity, and thecases when a FlutterFragment should only affect its own behavior. To preventa FlutterFragment from exposing its Activity to Flutter plugins, and toprevent Flutter from controlling the Activity’s system UI, use theshouldAttachEngineToActivity() method in FlutterFragment’s Builder, asshown: 9

MyActivity.java

  1. // Using a new FlutterEngine.
  2. FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
  3. .shouldAttachEngineToActivity(false)
  4. .build();
  5.  
  6. // Using a cached FlutterEngine.
  7. FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .shouldAttachEngineToActivity(false)
  9. .build();

MyActivity.kt

  1. // Using a new FlutterEngine.
  2. val flutterFragment = FlutterFragment.withNewEngine()
  3. .shouldAttachEngineToActivity(false)
  4. .build()
  5.  
  6. // Using a cached FlutterEngine.
  7. val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
  8. .shouldAttachEngineToActivity(false)
  9. .build()

Passing false to the shouldAttachEngineToActivity() Builder methodprevents Flutter from interacting with the surrounding Activity. The defaultvalue is true, which allows Flutter and Flutter plugins to interact with thesurrounding Activity.

备忘 Some plugins may expect or require an Activity reference. Ensure that none of your plugins require an Activity before you disable access.