Saltar al contenido principal

Añadir un Flutter Fragment a una aplicación Android

Aprende cómo añadir un Flutter Fragment a tu aplicación Android existente.

Encabezado para añadir Flutter Fragment

Esta guía describe cómo añadir un Fragment de Flutter a una aplicación Android existente. En Android, un Fragment representa una pieza modular de una interfaz de usuario más grande. Un Fragment puede usarse para presentar un panel deslizante (drawer), contenido con pestañas, una página en un ViewPager, o simplemente representar una pantalla normal en una aplicación de una sola Activity. Flutter proporciona un FlutterFragment para que los desarrolladores puedan presentar una experiencia Flutter en cualquier lugar donde puedan usar un Fragment normal.

Si una Activity es igualmente aplicable para las necesidades de tu aplicación, considera usar un FlutterActivity en lugar de un FlutterFragment, el cual es más rápido y fácil de usar.

FlutterFragment permite a los desarrolladores controlar los siguientes detalles de la experiencia Flutter dentro del Fragment:

  • Ruta inicial de Flutter
  • Punto de entrada de Dart a ejecutar
  • Fondo opaco frente a translúcido
  • Si FlutterFragment debe controlar su Activity circundante
  • Si se debe usar un nuevo FlutterEngine o un FlutterEngine en caché

FlutterFragment también viene con una serie de llamadas que deben ser reenviadas desde su Activity circundante. Estas llamadas permiten que Flutter reaccione adecuadamente a los eventos del sistema operativo.

En esta guía se describen todas las variantes de FlutterFragment y sus requisitos.

Añadir un FlutterFragment a una Activity con un nuevo FlutterEngine

#

Lo primero que debes hacer para usar un FlutterFragment es añadirlo a una Activity anfitriona.

Para añadir un FlutterFragment a una Activity anfitriona, instanciar y adjuntar una instancia de FlutterFragment en onCreate() dentro de la Activity, o en otro momento que funcione para tu aplicación:

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

El código anterior es suficiente para renderizar una interfaz de usuario de Flutter que comienza con una llamada a tu punto de entrada de Dart main(), una ruta inicial de Flutter de / y un nuevo FlutterEngine. Sin embargo, este código no es suficiente para lograr todo el comportamiento esperado de Flutter. Flutter depende de varias señales del sistema operativo que deben ser reenviadas desde tu Activity anfitriona a FlutterFragment. Estas llamadas se muestran en el siguiente ejemplo:

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

Con las señales del sistema operativo reenviadas a Flutter, tu FlutterFragment funcionará como se espera. Ahora has añadido un FlutterFragment a tu aplicación Android existente.

La ruta de integración más simple utiliza un nuevo FlutterEngine, el cual requiere un tiempo de inicialización no trivial, lo que resulta en una interfaz de usuario en blanco hasta que Flutter se inicializa y se renderiza por primera vez. La mayor parte de este retraso se puede evitar utilizando un FlutterEngine en caché y preparado, lo cual se analiza a continuación.

Uso de un FlutterEngine preparado

#

Por defecto, un FlutterFragment crea su propia instancia de un FlutterEngine, lo que requiere un tiempo de preparación no trivial. Esto significa que tu usuario verá un Fragment en blanco durante un breve momento. Puedes mitigar la mayor parte de este tiempo de preparación utilizando una instancia existente y preparada de FlutterEngine.

Para usar un FlutterEngine preparado en un FlutterFragment, instanciar un FlutterFragment con el método de fábrica withCachedEngine().

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 conoce internamente la clase FlutterEngineCache y recupera el FlutterEngine preparado en función del ID proporcionado a withCachedEngine().

Al proporcionar un FlutterEngine preparado, como se mostró anteriormente, tu aplicación renderizará el primer frame de Flutter lo más rápido posible.

Ruta inicial con un motor en caché

#

El concepto de una ruta inicial está disponible al configurar una FlutterActivity o un FlutterFragment con un nuevo FlutterEngine. Sin embargo, FlutterActivity y FlutterFragment no ofrecen el concepto de una ruta inicial cuando se utiliza un motor en caché. Esto se debe a que se espera que un motor en caché ya esté ejecutando código Dart, lo que significa que es demasiado tarde para configurar la ruta inicial.

Los desarrolladores que deseen que su motor en caché comience con una ruta inicial personalizada pueden configurar su FlutterEngine en caché para usar una ruta inicial personalizada justo antes de ejecutar el punto de entrada de Dart. El siguiente ejemplo demuestra el uso de una ruta inicial con un motor en caché:

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

Al establecer la ruta inicial del canal de navegación, el FlutterEngine asociado muestra la ruta deseada tras la ejecución inicial de la función Dart runApp().

Cambiar la propiedad de ruta inicial del canal de navegación después de la ejecución inicial de runApp() no tiene ningún efecto. Los desarrolladores que deseen utilizar el mismo FlutterEngine entre diferentes Activitys y Fragments y cambiar la ruta entre esas pantallas deben configurar un canal de método (method channel) y indicar explícitamente a su código Dart que cambie las rutas del Navigator.

Mostrar una pantalla de inicio (splash screen)

#

La visualización inicial del contenido de Flutter requiere cierto tiempo de espera, incluso si se utiliza un FlutterEngine preparado. Para ayudar a mejorar la experiencia del usuario en torno a este breve período de espera, Flutter admite la visualización de una pantalla de inicio (también conocida como "pantalla de lanzamiento") hasta que Flutter renderiza su primer frame. Para obtener instrucciones sobre cómo mostrar una pantalla de lanzamiento, consulta la guía de la pantalla de inicio (splash screen).

Ejecutar Flutter con una ruta inicial especificada

#

Una Android app podría contener muchas experiencias de Flutter independientes, ejecutándose en diferentes FlutterFragments, con diferentes FlutterEngines. En estos escenarios, es común que cada experiencia de Flutter comience con rutas iniciales diferentes (rutas distintas de /). Para facilitar esto, el Builder de FlutterFragment te permite especificar la ruta inicial deseada, como se muestra:

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

Ejecutar Flutter desde un punto de entrada especificado

#

De manera similar a la variación de las rutas iniciales, diferentes FlutterFragments pueden querer ejecutar diferentes puntos de entrada de Dart. En una aplicación típica de Flutter, solo hay un punto de entrada de Dart: main(), pero puedes definir otros puntos de entrada.

FlutterFragment admite la especificación del punto de entrada de Dart deseado a ejecutar para la experiencia Flutter dada. Para especificar un punto de entrada, construye el FlutterFragment de la siguiente manera:

MyActivity.kt
kotlin
val flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build()
MyActivity.java
java
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build();

La configuración de FlutterFragment da como resultado la ejecución de un punto de entrada de Dart llamado mySpecialEntrypoint(). Ten en cuenta que los paréntesis () no se incluyen en el nombre tipo String en dartEntrypoint.

Controlar el modo de renderizado de FlutterFragment

#

FlutterFragment puede usar un SurfaceView para renderizar su contenido de Flutter, o bien un TextureView. El valor predeterminado es SurfaceView, que es significativamente mejor para el rendimiento que TextureView. Sin embargo, SurfaceView no se puede intercalar en medio de una jerarquía de View de Android. Un SurfaceView debe ser la View más baja en la jerarquía, o la View más alta en la jerarquía. Además, en versiones de Android anteriores a Android N, los SurfaceViews no se pueden animar porque su diseño y renderizado no están sincronizados con el resto de la jerarquía de View. Si cualquiera de estos casos de uso son requisitos para tu aplicación, entonces debes usar un TextureView en lugar de un SurfaceView. Selecciona un TextureView construyendo un FlutterFragment con un RenderMode de texture:

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

Usando la configuración mostrada, el FlutterFragment resultante renderiza su UI en un TextureView.

Mostrar un FlutterFragment con transparencia

#

Por defecto, FlutterFragment se renderiza con un fondo opaco, utilizando un SurfaceView. (Consulta "Controlar el modo de renderizado de FlutterFragment"). Ese fondo es negro para cualquier píxel que no esté pintado por Flutter. Renderizar con un fondo opaco es el modo de renderizado preferido por razones de rendimiento. El renderizado de Flutter con transparencia en Android afecta negativamente al rendimiento. Sin embargo, hay muchos diseños que requieren píxeles transparentes en la experiencia Flutter para mostrarse a través de la UI subyacente de Android. Por esta razón, Flutter admite la translucidez en un FlutterFragment.

Para habilitar la transparencia en un FlutterFragment, construye el fragmento con la siguiente configuración:

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

La relación entre FlutterFragment y su Activity

#

Algunas aplicaciones eligen usar Fragments como pantallas completas de Android. In estas aplicaciones, sería razonable que un Fragment controle los elementos del sistema (system chrome) como la barra de estado de Android, la barra de navegación y la orientación.

Flutter a pantalla completa

En otras aplicaciones, los Fragments se utilizan para representar solo una parte de una UI. Un FlutterFragment podría usarse para implementar el interior de un panel deslizante (drawer), un reproductor de video o una sola tarjeta. En estas situaciones, sería inapropiado que el FlutterFragment afecte a los elementos del sistema (system chrome) de Android porque hay otras partes de la UI dentro de la misma Window.

Flutter como interfaz de usuario parcial

FlutterFragment viene con un concepto que ayuda a diferenciar el caso en que un FlutterFragment debería poder controlar su Activity anfitriona de los casos en que un FlutterFragment solo debería afectar a su propio comportamiento. Para evitar que un FlutterFragment exponga su Activity a los plugins de Flutter, y para evitar que Flutter controle la interfaz de usuario del sistema de la Activity, utiliza el método shouldAttachEngineToActivity() en el Builder de FlutterFragment, como se muestra:

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

Pasar false al método Builder shouldAttachEngineToActivity() evita que Flutter interactúe con la Activity circundante. El valor predeterminado es true, lo que permite que Flutter y los plugins de Flutter interactúen con la Activity circundante.