Saltar al contenido principal

Componentes diferidos para Android y web

Cómo crear componentes diferidos para mejorar el rendimiento de descarga.

Introducción

#

Con Flutter, las aplicaciones Android y web tienen la capacidad de descargar componentes diferidos (código y activos adicionales) mientras la aplicación ya se está ejecutando. Esto es útil si tienes una aplicación grande y solo deseas instalar componentes cuando el usuario los necesite.

Aunque Flutter admite la carga diferida en Android y la web, las implementaciones difieren. Ambas requieren importaciones diferidas de Dart.

  • Los dynamic feature modules de Android entregan los componentes diferidos empaquetados como módulos de Android.

    Al compilar para Android, aunque puedes diferir la carga de módulos, debes compilar toda la aplicación y cargarla como un único Android App Bundle (AAB). Flutter no admite el envío de actualizaciones parciales sin volver a subir nuevos Android App Bundles para toda la aplicación.

    Flutter realiza la carga diferida cuando compilas tu aplicación Android en modo release o profile, pero el modo debug trata todos los componentes diferidos como importaciones normales.

  • La web crea los componentes diferidos como archivos *.js independientes.

Para profundizar en los detalles técnicos de cómo funciona esta característica, consulta Componentes diferidos (Deferred Components) en la wiki de Flutter.

Cómo configurar tu proyecto Android para componentes diferidos

#

Las siguientes instrucciones explican cómo configurar tu aplicación Android para la carga diferida.

Paso 1: Dependencias y configuración inicial del proyecto

#
  1. Añade Play Core a las dependencias de build.gradle de la aplicación Android. En android/app/build.gradle añade lo siguiente:

    android/app/build.gradle.kts
    kotlin
    ...
    dependencies {
      ...
      implementation("com.google.android.play:core:1.8.0")
      ...
    }
    
    android/app/build.gradle
    groovy
    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    
  2. Si utilizas Google Play Store como modelo de distribución para funciones dinámicas, la aplicación debe admitir SplitCompat y proporcionar una instancia de un PlayStoreDeferredComponentManager. Ambas tareas se pueden realizar configurando la propiedad android:name en la aplicación en android/app/src/main/AndroidManifest.xml como io.flutter.embedding.android.FlutterPlayStoreSplitApplication:

    xml
    <manifest ...
      <application
         android:name="io.flutter.embedding.android.FlutterPlayStoreSplitApplication"
            ...
      </application>
    </manifest>
    

    io.flutter.app.FlutterPlayStoreSplitApplication maneja ambas tareas por ti. Si utilizas FlutterPlayStoreSplitApplication, puedes pasar al paso 1.3.

    Si tu aplicación Android es grande o compleja, es posible que desees admitir por separado SplitCompat y proporcionar el PlayStoreDynamicFeatureManager manualmente.

    Para admitir SplitCompat, existen tres métodos (como se detalla en la documentación de Android), cualquiera de los cuales es válido:

    • Haz que tu clase de aplicación extienda SplitCompatApplication:

      java
      public class MyApplication extends SplitCompatApplication {
          ...
      }
      
    • Llama a SplitCompat.install(this); en el método attachBaseContext():

      java
      @Override
      protected void attachBaseContext(Context base) {
          super.attachBaseContext(base);
          // Emulates installation of future on demand modules using SplitCompat.
          SplitCompat.install(this);
      }
      
    • Declara SplitCompatApplication como la subclase de aplicación y añade el código de compatibilidad de Flutter de FlutterApplication a tu clase de aplicación:

      xml
      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      

    El embedder se basa en una instancia inyectada de DeferredComponentManager para manejar las solicitudes de instalación de componentes diferidos. Proporciona un PlayStoreDeferredComponentManager en el embedder de Flutter agregando el siguiente código a la inicialización de tu aplicación:

    java
    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    import io.flutter.FlutterInjector;
    ...
    PlayStoreDeferredComponentManager deferredComponentManager = new
      PlayStoreDeferredComponentManager(this, null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    
  3. Opta por los componentes diferidos agregando la entrada deferred-components al archivo pubspec.yaml de la aplicación bajo la entrada flutter:

    yaml
    ...
    flutter:
      ...
      deferred-components:
      ...
    

    La herramienta flutter busca la entrada deferred-components en el archivo pubspec.yaml para determinar si la aplicación debe compilarse como diferida o no. Esto se puede dejar vacío por ahora a menos que ya conozcas los componentes deseados y las bibliotecas Dart diferidas que van en cada uno. Completarás esta sección más adelante en el paso 3.3 una vez que gen_snapshot produce las unidades de carga.

Paso 2: Implementar bibliotecas Dart diferidas

#

A continuación, implementa bibliotecas Dart cargadas de forma diferida en el código Dart de tu aplicación. No es necesario que la implementación esté completa aún. El ejemplo en el resto de esta página agrega un nuevo widget diferido simple como marcador de posición. También puedes convertir el código existente para que sea diferido modificando las importaciones y protegiendo el uso del código diferido detrás de loadLibrary() Futures.

  1. Crea una nueva biblioteca Dart. Por ejemplo, crea un nuevo widget DeferredBox que se pueda descargar en tiempo de ejecución. Este widget puede ser de cualquier complejidad pero, para los propósitos de esta guía, crea una caja simple como sustituto. Para crear un widget de caja azul simple, crea box.dart con el siguiente contenido:

    box.dart
    dart
    import 'package:flutter/material.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      const DeferredBox({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(height: 30, width: 30, color: Colors.blue);
      }
    }
    
  2. Importa la nueva biblioteca Dart con la palabra clave deferred en tu aplicación y llama a loadLibrary() (consulta carga diferida de una biblioteca). El siguiente ejemplo utiliza FutureBuilder para esperar a que se complete el loadLibrary Future (creado en initState) y mostrar un CircularProgressIndicator como marcador de posición. Cuando se completa el Future, devuelve el widget DeferredBox. SomeWidget puede usarse en la aplicación con normalidad y nunca intentará acceder al código Dart diferido hasta que se haya cargado correctamente.

    dart
    import 'package:flutter/material.dart';
    import 'box.dart' deferred as box;
    
    class SomeWidget extends StatefulWidget {
      const SomeWidget({super.key});
    
      @override
      State<SomeWidget> createState() => _SomeWidgetState();
    }
    
    class _SomeWidgetState extends State<SomeWidget> {
      late Future<void> _libraryFuture;
    
      @override
      void initState() {
        super.initState();
        _libraryFuture = box.loadLibrary();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<void>(
          future: _libraryFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              }
              return box.DeferredBox();
            }
            return const CircularProgressIndicator();
          },
        );
      }
    }
    

    La función loadLibrary() devuelve un Future<void> que se completa con éxito cuando el código de la biblioteca está disponible para su uso y se completa con un error en caso contrario. Todo uso de símbolos de la biblioteca diferida debe estar protegido detrás de una llamada loadLibrary() completada. Todas las importaciones de la biblioteca deben marcarse como deferred para que se compile adecuadamente para usarse en un componente diferido. Si un componente ya se ha cargado, las llamadas adicionales a loadLibrary() se completan rápidamente (pero no de forma síncrona). La función loadLibrary() también se puede llamar antes para activar una precarga que ayude a enmascarar el tiempo de carga.

    Puedes encontrar otro ejemplo de carga de importación diferida en lib/deferred_widget.dart de Flutter Gallery.

Paso 3: Construir la aplicación

#

Usa el comando de flutter para compilar una aplicación con componentes diferidos:

flutter build appbundle

Este comando te ayuda validando que tu proyecto esté configurado correctamente para compilar aplicaciones con componentes diferidos. De forma predeterminada, la compilación falla si el validador detecta algún problema y te guía a través de los cambios sugeridos para solucionarlo.

  1. El comando flutter build appbundle ejecuta el validador e intenta compilar la aplicación indicándole a gen_snapshot que produzca bibliotecas compartidas AOT divididas como archivos SO separados. En la primera ejecución, el validador probablemente fallará al detectar problemas; la herramienta hace recomendaciones sobre cómo configurar el proyecto y solucionar estos problemas.

    El validador se divide en dos secciones: validación antes de la compilación (prebuild) y después de gen_snapshot. Esto se debe a que cualquier validación que haga referencia a las unidades de carga no se puede realizar hasta que gen_snapshot se complete y produzca un conjunto final de unidades de carga.

    El validador detecta cualquier unidad de carga nueva, modificada o eliminada generada por gen_snapshot. Las unidades de carga generadas actualmente se registran en tu archivo <projectDirectory>/deferred_components_loading_units.yaml. Este archivo debe incluirse en el control de versiones para garantizar que se puedan detectar los cambios en las unidades de carga realizados por otros desarrolladores.

    El validador también comprueba lo siguiente en el directorio android:

    • <projectDir>/android/app/src/main/res/values/strings.xml
      Una entrada para cada componente diferido que mapea la clave ${componentName}Name a ${componentName}. Este recurso de cadena es utilizado por el AndroidManifest.xml de cada módulo de función para definir la propiedad dist:title. Por ejemplo:

      xml
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      
    • <projectDir>/android/<componentName>
      Existe un módulo de función dinámica de Android para cada componente diferido y contiene un archivo build.gradle y src/main/AndroidManifest.xml. Esto solo comprueba la existencia y no valida el contenido de estos archivos. Si un archivo no existe, genera uno recomendado por defecto.

    • <projectDir>/android/app/src/main/res/values/AndroidManifest.xml
      Contiene una entrada de meta-data que codifica el mapeo entre las unidades de carga y el nombre del componente con el que está asociada la unidad de carga. El embedder utiliza este mapeo para convertir el ID de la unidad de carga interna de Dart al nombre de un componente diferido para instalar. Por ejemplo:

      xml
      ...
      <application
          android:label="MyApp"
          android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
          android:icon="@mipmap/ic_launcher">
          ...
          <meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="2:boxComponent"/>
      </application>
      ...
      

    El validador de gen_snapshot no se ejecutará hasta que pase el validador previo a la compilación (prebuild).

  2. Para cada una de estas comprobaciones, la herramienta produce los archivos modificados o nuevos necesarios para pasar la comprobación. Estos archivos se colocan en el directorio <projectDir>/build/android_deferred_components_setup_files. Se recomienda aplicar los cambios copiando y sobreescribiendo los mismos archivos en el directorio android del proyecto. Antes de sobreescribir, el estado actual del proyecto debe confirmarse en el control de código fuente y los cambios recomendados deben revisarse para que sean apropiados. La herramienta no realizará ningún cambio en tu directorio android/ automáticamente.

  3. Una vez que las unidades de carga disponibles se generan y se registran en <projectDirectory>/deferred_components_loading_units.yaml, es posible configurar completamente la sección deferred-components del pubspec para que las unidades de carga se asignen a los componentes diferidos como se desee. Para continuar con el ejemplo de la caja, el archivo generado deferred_components_loading_units.yaml contendría:

    yaml
    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    

    El ID de la unidad de carga ('2' en este caso) se utiliza internamente por Dart y se puede ignorar. La unidad de carga base (ID '1') no aparece en la lista y contiene todo lo que no esté contenido explícitamente en otra unidad de carga.

    Ahora puedes agregar lo siguiente a pubspec.yaml:

    yaml
    ...
    flutter:
      ...
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      ...
    

    Para asignar una unidad de carga a un componente diferido, agrega cualquier biblioteca Dart de la unidad de carga en la sección de bibliotecas (libraries) del módulo de función. Ten en cuenta las siguientes pautas:

    • Las unidades de carga no deben incluirse en más de un componente.

    • Incluir una biblioteca Dart de una unidad de carga indica que toda la unidad de carga está asignada al componente diferido.

    • Todas las unidades de carga no asignadas a un componente diferido se incluyen en el componente base, que siempre existe implícitamente.

    • Las unidades de carga asignadas al mismo componente diferido se descargan, se instalan y se envían juntas.

    • El componente base es implícito y no necesita definirse en el pubspec.

  4. Los activos (assets) también pueden incluirse agregando una sección de activos (assets) en la configuración del componente diferido:

    yaml
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
          assets:
            - assets/image.jpg
            - assets/picture.png
              # wildcard directory
            - assets/gallery/
    

    Un activo se puede incluir en múltiples componentes diferidos, pero la instalación de ambos componentes da como resultado un activo duplicado. Los componentes que solo contienen activos también se pueden definir omitiendo la sección de bibliotecas. Estos componentes de solo activos deben instalarse con la clase de utilidad DeferredComponent en los servicios en lugar de loadLibrary(). Dado que las bibliotecas Dart se empaquetan junto con los activos, si una biblioteca Dart se carga con loadLibrary(), también se cargan los activos del componente. Sin embargo, instalar por nombre de componente y la utilidad de servicios no cargará ninguna biblioteca Dart en el componente.

    Eres libre de incluir activos en cualquier componente, siempre que estén instalados y cargados cuando se haga referencia a ellos por primera vez, aunque normalmente es mejor empaquetar los activos y el código Dart que los utiliza en el mismo componente.

  5. Agrega manualmente todos los componentes diferidos que definiste en pubspec.yaml dentro del archivo android/settings.gradle como includes. Por ejemplo, si hay tres componentes diferidos definidos en el pubspec llamados boxComponent, circleComponent y assetComponent, asegúrate de que android/settings.gradle contenga lo siguiente:

    android/settings.gradle.kts
    kotlin
    include(":app", ":boxComponent", ":circleComponent", ":assetComponent")
    ...
    
    android/settings.gradle
    groovy
    include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
    ...
    
  6. Repite los pasos del 3.1 al 3.6 (este paso) hasta que se gestionen todas las recomendaciones del validador y la herramienta se ejecute sin más recomendaciones.

    Cuando tiene éxito, este comando genera un archivo app-release.aab en build/app/outputs/bundle/release.

    Una compilación exitosa no siempre significa que la aplicación se haya compilado de la manera esperada. Depende de ti asegurarte de que todas las unidades de carga y bibliotecas Dart se incluyan de la forma esperada. Por ejemplo, un error común es importar accidentalmente una biblioteca Dart sin la palabra clave deferred, lo que da como resultado que una biblioteca diferida se compile como parte de la unidad de carga base. En este caso, la biblioteca Dart se cargaría correctamente porque siempre está presente en la base y la biblioteca no se dividiría. Esto se puede comprobar examinando el archivo deferred_components_loading_units.yaml para verificar que las unidades de carga generadas se describan como se esperaba.

    Al ajustar las configuraciones de los componentes diferidos, o al realizar cambios en Dart que agreguen, modifiquen o eliminen unidades de carga, es de esperar que el validador falle. Sigue los pasos del 3.1 al 3.6 (este paso) para aplicar cualquier cambio recomendado para continuar con la compilación.

Ejecutar la aplicación localmente

#

Una vez que tu aplicación haya compilado con éxito un archivo AAB, usa bundletool de Android para realizar pruebas locales con la bandera --local-testing.

Para ejecutar el archivo AAB en un dispositivo de prueba, descarga el ejecutable jar de bundletool desde github.com/google/bundletool/releases y ejecuta:

java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing

java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks

Donde <your_app_project_dir> es la ruta al directorio del proyecto de tu aplicación y <your_temp_dir> es cualquier directorio temporal utilizado para almacenar las salidas de bundletool. Esto desempaqueta tu archivo AAB en un archivo APK y lo instala en el dispositivo. Todas las funciones dinámicas de Android disponibles se cargan en el dispositivo localmente y se emula la instalación de componentes diferidos.

Antes de ejecutar build-apks nuevamente, elimina el archivo APK de la aplicación existente:

rm <your_temp_dir>/app.apks

Los cambios en la base de código Dart requieren incrementar el ID de compilación de Android o desinstalar y reinstalar la aplicación, ya que Android no actualizará los módulos de funciones a menos que detecte un nuevo número de versión.

Publicar en Google Play Store

#

El archivo AAB compilado se puede subir directamente a Play Store como de costumbre. Cuando se llama a loadLibrary(), el motor de Flutter descarga el módulo de Android necesario que contiene la biblioteca Dart AOT y los activos (assets) utilizando la función de entrega de Play Store.