Cómo configurar la entrega a pedido

Los módulos de funciones te permiten separar determinados recursos y funciones del módulo base de tu app y, luego, incluirlos en el paquete de aplicación. Mediante Play Feature Delivery, los usuarios pueden, por ejemplo, descargar e instalar esos componentes a pedido después de haber instalado el APK base de tu app.

Por ejemplo, piensa en una app de mensajería de texto que incluye funcionalidad para capturar y enviar mensajes con imágenes, pero solo un pequeño porcentaje de usuarios envía mensajes con imágenes. Tal vez tenga sentido incluir los mensajes con imágenes en un módulo de funciones descargable. De esa manera, la descarga inicial de la app es más chica para todos los usuarios, y solo aquellos que envían mensajes con imágenes deben descargar ese componente adicional.

Ten en cuenta que este tipo de modularización requiere más esfuerzo, y posiblemente tengas que refactorizar el código existente de tu app, de modo que deberás considerar con cuidado cuáles de las funciones de la app sería mejor ofrecer a pedido. Para comprender mejor los casos de uso óptimos y las pautas para las funciones on demand, consulta las prácticas recomendadas de UX para la entrega de funciones on demand.

Si deseas modularizar gradualmente las funciones de la app con el paso del tiempo, sin habilitar opciones de entrega avanzadas, como la entrega a pedido, configura la entrega durante la instalación.

En esta página, encontrarás ayuda para agregar un módulo de funciones a tu proyecto de app y configurarlo para la entrega a pedido. Antes de comenzar, asegúrate de usar Android Studio 3.5 o una versión posterior y el complemento de Gradle para Android versión 3.5.0 o posterior.

Cómo configurar un nuevo módulo para entrega a pedido

La forma más fácil de crear un nuevo módulo de funciones es con Android Studio 3.5 o una versión posterior. Dado que los módulos de funciones tienen una dependencia inherente del módulo de la app de base, solo puedes agregarlos a proyectos de apps existentes.

Para agregar un módulo de funciones a tu proyecto de app con Android Studio, haz lo siguiente:

  1. Si aún no lo hiciste, abre tu proyecto de app en el IDE.
  2. Selecciona File > New > New Module en la barra de menú.
  3. En el cuadro de diálogo Create New Module, elige Dynamic Feature Module y haz clic en Next.
  4. En la sección Configure your new module, haz lo siguiente:
    1. En el menú desplegable Base application module, selecciona el módulo base de tu app.
    2. En Module name, especifica el nombre del módulo. El IDE usa ese nombre para identificar el módulo como un subproyecto de Gradle en tu archivo de configuración de Gradle. Cuando compilas tu paquete de aplicación, Gradle usa el último elemento del nombre del subproyecto para insertar el atributo <manifest split> en el manifiesto del módulo de funciones.
    3. En Package name, especifica el nombre de paquete del módulo. De forma predeterminada, Android Studio sugiere un nombre de paquete que combina el nombre del paquete raíz del módulo base y el nombre del módulo que especificaste en el paso anterior.
    4. En Minimum API level, elige el nivel mínimo de API que admitirá el módulo. Este valor debe coincidir con el del módulo base.
  5. Haz clic en Next.
  6. En la sección Module Download Options, haz lo siguiente:

    1. Especifica el título del módulo en un máximo de 50 caracteres. La plataforma usa ese título para identificar el módulo ante los usuarios cuando, por ejemplo, confirma si el usuario desea descargar el módulo. Por esta razón, el módulo base de la app debe incluir el título del módulo como un recurso de strings, que se puede traducir. Cuando se crea el módulo con Android Studio, el IDE agrega el recurso de strings al módulo base y, luego, inserta la siguiente entrada en el manifiesto del módulo de funciones:

      <dist:module
          ...
          dist:title="@string/feature_title">
      </dist:module>
      
    2. En el menú desplegable en Install-time inclusion, selecciona Do not include module at install-time. Android Studio introduce lo siguiente en el manifiesto del módulo para reflejar tu elección:

      <dist:module ... >
        <dist:delivery>
            <dist:on-demand/>
        </dist:delivery>
      </dist:module>
      
    3. Marca la casilla junto a Fusing si deseas que ese módulo esté disponible para los dispositivos que ejecutan Android 4.4 (nivel de API 20) y versiones anteriores y se lo incluya en APK múltiples. Eso significa que puedes habilitar el comportamiento a pedido para este módulo e inhabilitar la fusión a fin de omitirlo en dispositivos que no admiten la descarga e instalación de APK divididos. Android Studio introduce lo siguiente en el manifiesto del módulo para reflejar tu elección:

      <dist:module ...>
          <dist:fusing dist:include="true | false" />
      </dist:module>
      
  7. Haz clic en Finish.

Cuando Android Studio termine de crear tu módulo, abre el panel Project y revisa el contenido (selecciona View > Tool Windows > Project en la barra de menú). El código, los recursos y la organización predeterminados deben ser similares a los del módulo de app estándar.

Luego, deberás implementar la función de instalación a pedido mediante la biblioteca de Play Core.

Cómo incluir la biblioteca de Play Core en tu proyecto

Antes de comenzar, debes agregar la biblioteca de Play Core a tu proyecto.

Cómo solicitar un módulo a pedido

Si tu app necesita usar un módulo de funciones, puede solicitar uno mientras está en primer plano mediante la clase SplitInstallManager. Cuando tu app envía una solicitud, debe especificar el nombre del módulo como lo define el elemento split en el manifiesto del módulo objetivo. Cuando creas un módulo de funciones con Android Studio, el sistema de compilación usa el nombre del módulo que proporcionas para incorporar esta propiedad en el manifiesto del módulo durante el tiempo de compilación. Para obtener más información, lee sobre los manifiestos del módulo de funciones.

Por ejemplo, imagina una app que tiene un módulo a pedido para capturar y enviar mensajes con imágenes mediante la cámara del dispositivo y este módulo a pedido tiene split="pictureMessages" especificado en su manifiesto. En el siguiente ejemplo, se usa SplitInstallManager a fin de solicitar el módulo de pictureMessages (además del módulo adicional para algunos filtros promocionales):

Kotlin

// Creates an instance of SplitInstallManager.
val splitInstallManager = SplitInstallManagerFactory.create(context)

// Creates a request to install a module.
val request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build()

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener { sessionId -> ... }
    .addOnFailureListener { exception ->  ... }

Java

// Creates an instance of SplitInstallManager.
SplitInstallManager splitInstallManager =
    SplitInstallManagerFactory.create(context);

// Creates a request to install a module.
SplitInstallRequest request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build();

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener(sessionId -> { ... })
    .addOnFailureListener(exception -> { ... });

Cuando tu app solicita un módulo a pedido, la biblioteca de Play Core utiliza una estrategia de "activar y olvidar". Es decir, envía la solicitud para descargar el módulo a la plataforma, pero no supervisa si la instalación se realizó correctamente. Para avanzar en el recorrido del usuario después de la instalación o administrar errores correctamente, asegúrate de supervisar el estado de la solicitud.

Nota: Es posible solicitar un módulo de funciones que ya está instalado en el dispositivo. La API considera inmediatamente que se completó la solicitud si detecta que el módulo ya está instalado. Además, después de instalar un módulo, Google Play lo actualiza automáticamente. Es decir, cuando subes una versión nueva de tu paquete de aplicación, la plataforma actualiza todos los APK instalados que pertenecen a tu app. Si deseas obtener más información, consulta Cómo administrar actualizaciones de apps.

A fin de obtener acceso al código y a los recursos del módulo, debes habilitar SplitCompat para tu app. Ten en cuenta que SplitCompat no es necesario para las Apps instantáneas Android.

Cómo posponer la instalación de módulos a pedido

Si no necesitas que tu app descargue e instale inmediatamente un módulo a pedido, puedes postergar la instalación para cuando la app esté en segundo plano. Por ejemplo, si quieres precargar material promocional para un lanzamiento posterior de tu app.

Puedes especificar que un módulo se descargue más adelante mediante el método deferredInstall(), como se muestra a continuación. Además, a diferencia de SplitInstallManager.startInstall(), tu app no necesita estar en primer plano para iniciar la solicitud de una instalación postergada.

Kotlin

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(listOf("promotionalFilters"))

Java

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(Arrays.asList("promotionalFilters"));

Las solicitudes de instalaciones postergadas se incluyen en la categoría de mejor esfuerzo y no puedes realizar un seguimiento de su progreso. Por lo tanto, antes de intentar acceder a un módulo que especificaste para la instalación diferida, debes verificar que el módulo se haya instalado. Si necesitas que el módulo esté disponible inmediatamente, usa SplitInstallManager.startInstall() en su lugar para solicitarlo, como se muestra en la sección anterior.

Cómo supervisar el estado de la solicitud

Para poder actualizar una barra de progreso, activar un intent después de la instalación o manejar correctamente un error de solicitud, debes detectar actualizaciones de estado desde la tarea SplitInstallManager.startInstall() asíncrona. Antes de comenzar a recibir actualizaciones para tu solicitud de instalación, registra un objeto de escucha y obtén el ID de sesión de la solicitud, como se muestra a continuación.

Kotlin

// Initializes a variable to later track the session ID for a given request.
var mySessionId = 0

// Creates a listener for request status updates.
val listener = SplitInstallStateUpdatedListener { state ->
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
}

// Registers the listener.
splitInstallManager.registerListener(listener)

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener { sessionId -> mySessionId = sessionId }
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener { exception ->
        // Handle request errors.
    }

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener)

Java

// Initializes a variable to later track the session ID for a given request.
int mySessionId = 0;

// Creates a listener for request status updates.
SplitInstallStateUpdatedListener listener = state -> {
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
};

// Registers the listener.
splitInstallManager.registerListener(listener);

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener(sessionId -> { mySessionId = sessionId; })
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener(exception -> {
        // Handle request errors.
    });

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener);

Cómo manejar los errores de solicitud

Ten en cuenta que la instalación de módulos de funciones a pedido puede fallar en algún momento, del mismo modo que la instalación de la app no siempre se realiza de forma correcta. La falla en la instalación puede deberse a problemas de poco almacenamiento en el dispositivo, conectividad de red o al usuario que no accedió a Google Play Store. Para obtener sugerencias sobre cómo manejar correctamente estas situaciones desde la perspectiva del usuario, consulta nuestros Lineamientos de UX para las entregas a pedido.

En cuanto al código, deberías manejar las fallas para descargar o instalar un módulo con addOnFailureListener(), como se muestra a continuación:

Kotlin

splitInstallManager
    .startInstall(request)
    .addOnFailureListener { exception ->
        when ((exception as SplitInstallException).errorCode) {
            SplitInstallErrorCode.NETWORK_ERROR -> {
                // Display a message that requests the user to establish a
                // network connection.
            }
            SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads()
            ...
        }
    }

fun checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .sessionStates
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                // Check for active sessions.
                for (state in task.result) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        }
}

Java

splitInstallManager
    .startInstall(request)
    .addOnFailureListener(exception -> {
        switch (((SplitInstallException) exception).getErrorCode()) {
            case SplitInstallErrorCode.NETWORK_ERROR:
                // Display a message that requests the user to establish a
                // network connection.
                break;
            case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
                checkForActiveDownloads();
            ...
    });

void checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .getSessionStates()
        .addOnCompleteListener( task -> {
            if (task.isSuccessful()) {
                // Check for active sessions.
                for (SplitInstallSessionState state : task.getResult()) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        });
}

En la siguiente tabla, se describen los estados de error que es posible que tu app deba manejar:

Código de error Descripción Acción sugerida
ACTIVE_SESSIONS_LIMIT_EXCEEDED Se rechaza la solicitud porque, actualmente, se está descargando, al menos, una solicitud existente. Verifica si todavía se están descargando solicitudes, como se muestra en el ejemplo anterior.
MODULE_UNAVAILABLE Google Play no encuentra el módulo que se solicitó en función de la versión instalada actualmente de la app, el dispositivo y la cuenta de Google Play del usuario. Si el usuario no tiene acceso al módulo, debes notificarlo al respecto.
INVALID_REQUEST Google Play recibió la solicitud, pero esta no es válida. Verifica que la información incluida en la solicitud esté completa y sea precisa.
SESSION_NOT_FOUND No se encontró ninguna sesión para un ID de sesión determinado. Si intentas supervisar el estado de una solicitud a partir de su ID de sesión, asegúrate de que este sea correcto.
API_NOT_AVAILABLE El dispositivo actual no admite la biblioteca de Play Core. Es decir, el dispositivo no puede descargar ni instalar funciones a pedido. Para los dispositivos con Android 4.4 (nivel de API 20) o versiones anteriores, debes incluir módulos de funciones en el tiempo de instalación. Para ello, usa la propiedad de manifiesto dist:fusing. Si deseas obtener más información, lee sobre el manifiesto del módulo de funciones.
ACCESS_DENIED La app no puede registrar la solicitud debido a que los permisos no son suficientes. Por lo general, esto sucede cuando la app está en segundo plano. Intenta ejecutar la solicitud cuando la app vuelva al primer plano.
NETWORK_ERROR Falló la solicitud debido a un error de red. Pídele al usuario que establezca una conexión de red o que se conecte a una diferente.
INCOMPATIBLE_WITH_EXISTING_SESSION La solicitud contiene uno o más módulos que ya se solicitaron, pero que todavía no se instalaron. Crea una solicitud nueva que no incluya módulos que tu app ya solicitó o espera a que todos los módulos solicitados actualmente terminen de instalarse antes de reintentarlo.

Ten en cuenta que solicitar un módulo que ya se instaló no soluciona un error.

SERVICE_DIED El servicio responsable de manejar la solicitud no está disponible. Vuelve a intentar ejecutar la solicitud.

Tu SplitInstallStateUpdatedListener recibe un SplitInstallSessionState con este código de error, un estado FAILED y un ID de sesión -1.

INSUFFICIENT_STORAGE El dispositivo no tiene suficiente almacenamiento gratuito para instalar el módulo de funciones. Notifica al usuario que no tiene almacenamiento suficiente para instalar esta función.
APP_NOT_OWNED Google Play no instaló la app y no se puede descargar la función. En la versión 1.9 de Play Core o posterior, este error solo se puede producir para las instalaciones diferidas. Si quieres que el usuario adquiera la app en Google Play, usa startInstall(), que puede obtener la confirmación de usuario necesaria (versión de Play Core 1.9 o posterior).

Si un usuario solicita descargar un módulo a pedido y se produce un error, puedes mostrar un diálogo en el que se brinden dos opciones para el usuario: Try again (para volver a realizar la solicitud) y Cancel (para abandonar la solicitud). Para obtener asistencia adicional, también debes proporcionar un vínculo de ayuda que dirija a los usuarios al Centro de ayuda de Google Play.

Cómo manejar las actualizaciones de estado

Después de registrar un objeto de escucha y grabar el ID de sesión de tu solicitud, usa StateUpdatedListener.onStateUpdate() a fin de manejar los cambios de estado, como se muestra a continuación.

Kotlin

override fun onStateUpdate(state : SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIED) {
       // Retry the request.
       return
    }
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.DOWNLOADING -> {
              val totalBytes = state.totalBytesToDownload()
              val progress = state.bytesDownloaded()
              // Update progress bar.
            }
            SplitInstallSessionStatus.INSTALLED -> {

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIES) {
       // Retry the request.
       return;
    }
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.DOWNLOADING:
              int totalBytes = state.totalBytesToDownload();
              int progress = state.bytesDownloaded();
              // Update progress bar.
              break;

            case SplitInstallSessionStatus.INSTALLED:

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
        }
    }
}

Los posibles estados de tu solicitud de instalación se describen en la siguiente tabla.

Estado de solicitud Descripción Acción sugerida
PENDIENTE Se aceptó la solicitud y la descarga debería comenzar pronto. Inicializa los componentes de IU, como una barra de progreso, para brindar los comentarios del usuario sobre la descarga.
REQUIRES_USER_CONFIRMATION La descarga requiere la confirmación del usuario. Generalmente, este estado ocurre si la app no se instaló a través de Google Play. Pídele al usuario que confirme la descarga de la función mediante GooglebPlay. Si deseas conocer más detalles, ve a la sección sobre cómo obtener información del usuario.
DOWNLOADING La descarga está en curso. Si agregas una barra de progreso para la descarga, usa los métodos SplitInstallSessionState.bytesDownloaded() y SplitInstallSessionState.totalBytesToDownload() a fin de actualizar la IU (consulta la muestra de código ubicada arriba de esta tabla).
DOWNLOADED El dispositivo descargó el módulo, pero la instalación todavía no comenzó. Las apps deberían habilitar SplitCompat para tener acceso a los módulos descargados y evitar ver este estado. Esto es necesario para acceder al código y a los recursos del módulo de funciones.
INSTALANDO El dispositivo está instalando el módulo. Actualiza la barra de progreso. Por lo general, este estado es corto.
INSTALLED Se instaló el módulo en el dispositivo. Accede al código y a los recursos en el módulo para continuar el recorrido del usuario.

Si el módulo es para una App instantánea Android que ejecuta Android 8.0 (nivel de API 26) o versiones posteriores, usa splitInstallHelper a fin de actualizar los componentes de la app con el módulo nuevo.

ERROR Falló la solicitud antes de que se pudiera instalar el módulo en el dispositivo. Pídele al usuario que vuelva a enviar la solicitud o que la cancele.
CANCELING El dispositivo está en proceso de cancelar la solicitud. Para obtener más información, ve a la sección sobre cómo cancelar una solicitud de instalación.
CANCELED Se canceló la solicitud.

Cómo obtener la confirmación del usuario

En algunos casos, es posible que Google Play requiera la confirmación del usuario antes de completar una solicitud de descarga. Por ejemplo, si Google Play no instaló tu app o si intentas realizar una descarga grande usando datos móviles. En estos casos, el estado de la solicitud informa REQUIRES_USER_CONFIRMATION, y tu app necesita obtener la confirmación del usuario para que el dispositivo pueda descargar y también instalar los módulos en la solicitud. Para obtener la confirmación, tu app debe pedirle al usuario lo siguiente:

Kotlin

override fun onSessionStateUpdate(state: SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          /* activity = */ this,
          // You use this request code to later retrieve the user's decision.
          /* requestCode = */ MY_REQUEST_CODE)
    }
    ...
 }

Java

@Override void onSessionStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          /* activity = */ this,
          // You use this request code to later retrieve the user's decision.
          /* requestCode = */ MY_REQUEST_CODE);
    }
    ...
 }

El estado de la solicitud se actualiza según la respuesta del usuario:

  • Si el usuario acepta la confirmación, el estado de la solicitud cambia a PENDING y la descarga continúa.
  • Si el usuario rechaza la confirmación, el estado de la solicitud cambia a CANCELED.
  • Si el usuario no hace una selección antes de que se destruya el diálogo, el estado de la solicitud permanece como REQUIRES_USER_CONFIRMATION. Tu app puede volver a pedirle al usuario que complete la solicitud.

Para recibir una devolución de llamada con la respuesta del usuario, utiliza onActivityResult(), como se muestra a continuación.

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  if (requestCode == MY_REQUEST_CODE) {
    // Handle the user's decision. For example, if the user selects "Cancel",
    // you may want to disable certain functionality that depends on the module.
  }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == MY_REQUEST_CODE) {
    // Handle the user's decision. For example, if the user selects "Cancel",
    // you may want to disable certain functionality that depends on the module.
  }
}

Cómo cancelar una solicitud de instalación

Si tu app necesita cancelar una solicitud antes de que se instale, puede invocar el método cancelInstall() mediante el ID de sesión de la solicitud, como se muestra a continuación.

Kotlin

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId)

Java

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId);

Cómo acceder a los módulos

Para acceder al código y los recursos de un módulo descargado después de la descarga, tu app debe habilitar la biblioteca de SplitCompat para tu app y cada actividad en los módulos de funciones que descarga tu app.

Sin embargo, ten en cuenta que la plataforma cuenta con las siguientes restricciones para acceder al contenido de un módulo durante un tiempo (días, en algunos casos) después de descargar el módulo:

  • La plataforma no puede aplicar ninguna entrada nueva del manifiesto que ingrese el módulo.
  • La plataforma no puede acceder a los recursos del módulo para los componentes de la IU del sistema, como las notificaciones. Si necesitas usar esos recursos inmediatamente, puedes incluirlos en el módulo de base de tu app.

Habilita SplitCompat

Si quieres que tu app acceda al código y los recursos de un módulo descargado, debes habilitar SplitCompat mediante solo uno de los métodos que se describen en las siguientes secciones.

Después de habilitar SplitCompat en tu app, también debes habilitar SplitCompat para cada actividad en los módulos de funciones a los que quieres que tu app tenga acceso.

Cómo declarar SplitCompatApplication en el manifiesto

La manera más simple de habilitar SplitCompat consiste en declarar SplitCompatApplication como la subclase de Application en el manifiesto de tu app, como se muestra a continuación:

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

Después de instalar la app en un dispositivo, puedes acceder automáticamente al código y los recursos de los módulos de funciones descargados.

Cómo invocar SplitCompat durante el tiempo de ejecución

También puedes habilitar SplitCompat en actividades o servicios específicos durante el tiempo de ejecución. Es necesario habilitar SplitCompat de esta manera para iniciar las actividades incluidas en los módulos de funciones. Para ello, anula attachBaseContext como se muestra a continuación.

Si tienes una clase Application personalizada, haz que extienda SplitCompatApplication a fin de habilitar SplitCompat para tu app, como se muestra a continuación:

Kotlin

class MyApplication : SplitCompatApplication() {
    ...
}

Java

public class MyApplication extends SplitCompatApplication {
    ...
}

SplitCompatApplication simplemente anula ContextWrapper.attachBaseContext() para incluir SplitCompat.install(Context applicationContext). Si no deseas que tu clase Application extienda SplitCompatApplication, puedes anular el método attachBaseContext() de forma manual, como se muestra a continuación:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
}

Si tu módulo a pedido es compatible con las apps instantáneas y las apps instaladas, puedes invocar SplitCompat de manera condicional, como se muestra a continuación:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this)
    }
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this);
    }
}

Cómo habilitar SplitCompat para las actividades del módulo

Después de habilitar SplitCompat para tu app de base, debes habilitar SplitCompat en cada actividad que tu app descargue en un módulo de funciones. Para ello, usa el método SplitCompat.installActivity() de la siguiente manera:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this);
}

Accede a los componentes definidos en los módulos de funciones

Inicia una actividad definida en un módulo de funciones

Puedes iniciar actividades definidas en los módulos de funciones mediante startActivity() después de habilitar SplitCompat.

Kotlin

startActivity(Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...))

Java

startActivity(new Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...));

El primer parámetro de setClassName es el nombre del paquete de la app, y el segundo es el nombre completo de la clase de la actividad.

Cuando tienes una actividad en un módulo de funciones que descargaste a pedido, debes habilitar SplitCompat en la actividad.

Inicia un servicio definido en un módulo de funciones

Puedes iniciar servicios definidos en los módulos de funciones mediante startService() después de habilitar SplitCompat.

Kotlin

startService(Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...))

Java

startService(new Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...));

Exporta un componente definido en un módulo de funciones

No debes incluir componentes de Android exportados en módulos opcionales.

El sistema de compilación combina entradas de manifiesto para todos los módulos en el módulo base. Si un módulo opcional contuviera un componente exportado, sería accesible incluso antes de que se instale el módulo y podría causar una falla debida a la falta del código cuando se invoca desde otra app.

Este no es un problema para los componentes internos: solo la app accede a ellos, por lo que esta puede verificar que el módulo esté instalado antes de acceder al componente.

Si necesitas un componente exportado y deseas que su contenido esté en un módulo opcional, procura implementar un patrón de proxy. Puedes hacerlo agregando un componente exportado del proxy en la base. Cuando se accede a él, este puede verificar la presencia del módulo con el contenido. Si el módulo está presente, el componente del proxy puede iniciar el componente interno desde el módulo a través de un Intent y retransmitir el intent desde la app que realiza la llamada. Si el módulo no está presente, el componente puede descargarlo o mostrar un mensaje de error apropiado a dicha app.

Cómo acceder al código y a los recursos desde los módulos instalados

Si habilitas SplitCompat para el contexto de tu aplicación de base y las actividades en tu módulo de funciones, puedes usar el código y los recursos desde un módulo de funciones como si fueran parte del APK de base una vez que se instala el módulo opcional.

Accede al código desde otro módulo

Accede al código base desde un módulo

Es posible que otros módulos usen de forma directa el código que se encuentra dentro de tu módulo base. No necesitas hacer nada especial: solo importa y usa las clases que necesites.

Accede al código del módulo desde otro módulo

No es posible que otros módulos accedan directamente a un objeto o a una clase dentro de un módulo de forma estática, pero pueden acceder a él de manera indirecta mediante la reflexión.

Debes tener cuidado con la frecuencia con la que esto sucede, debido a los costos que la reflexión impone sobre el rendimiento. En casos de uso complejos, usa frameworks de inyección de dependencias, como Dagger 2, a fin de garantizar una sola llamada de reflexión por cada ciclo de vida de la aplicación.

Para simplificar las interacciones con el objeto después de que se cree la instancia, te recomendamos que definas una interfaz en el módulo base y su implementación en el módulo de funciones. Por ejemplo:

Kotlin

// In the base module
interface MyInterface {
  fun hello(): String
}

// In the feature module
object MyInterfaceImpl : MyInterface {
  override fun hello() = "Hello"
}

// In the base module, where we want to access the feature module code
val stringFromModule = (Class.forName("com.package.module.MyInterfaceImpl")
    .kotlin.objectInstance as MyInterface).hello();

Java

// In the base module
public interface MyInterface {
  String hello();
}

// In the feature module
public class MyInterfaceImpl implements MyInterface {
  @Override
  public String hello() {
    return "Hello";
  }
}

// In the base module, where we want to access the feature module code
String stringFromModule =
   ((MyInterface) Class.forName("com.package.module.MyInterfaceImpl").getConstructor().newInstance()).hello();

Accede a los recursos y elementos desde otro módulo

Una vez que se instala un módulo, puedes acceder a los recursos y elementos dentro de este de la manera estándar, pero hay dos advertencias:

  • Si accedes a un recurso desde un módulo diferente, el módulo no tendrá acceso al identificador de recursos, aunque aún se podrá acceder al recurso mediante su nombre. Ten en cuenta que el paquete que se debe usar para hacer referencia al recurso es el del módulo en el que se define el recurso.
  • Si deseas acceder a elementos o recursos que existen en un módulo recién instalado desde otro módulo instalado de tu app, debes hacerlo usando el contexto de la aplicación. El contexto del componente que intenta acceder a los recursos todavía no estará actualizado. Como alternativa, puedes volver a crear ese componente (por ejemplo, llamando a Activity.recreate()) o reinstall SplitCompat en él después de la instalación del módulo de funciones.

Carga código nativo desde un módulo opcional

Una vez que se instala una división, se puede cargar su código nativo invocando el System.loadLibrary(libName) estándar. Para las apps instantáneas, proporcionamos un método especial.

Si usas System.loadLibrary() a fin de cargar tu código nativo y la biblioteca nativa tiene una dependencia en otra biblioteca del módulo, primero debes cargar manualmente esa otra biblioteca.

Si usas dlopen() en código nativo a fin de cargar una biblioteca definida en un módulo opcional, no funcionará con rutas de acceso relativas de bibliotecas. La mejor solución es recuperar la ruta de acceso absoluta de la biblioteca desde el código Java a través de ClassLoader.findLibrary() y, luego, usarla en tu llamada a dlopen(). Haz esto antes de ingresar el código nativo o usa una llamada JNI desde tu código nativo a Java.

Cómo acceder a Apps instantáneas Android instaladas

Después de que se informe un módulo de App instantánea Android como INSTALLED, puedes acceder a su código y sus recursos mediante el contexto actualizado de una app. Un contexto que tu app crea antes de instalar un módulo (por ejemplo, uno que ya está instalado en una variable) no incluye el contenido del módulo nuevo. Sin embargo, un contexto actualizado sí lo incluye y lo puedes obtener, por ejemplo, mediante createPackageContext.

Kotlin

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                val newContext = context.createPackageContext(context.packageName, 0)
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                val am = newContext.assets
            }
        }
    }
}

Java

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                AssetManager am = newContext.getAssets();
        }
    }
}

Apps instantáneas Android en Android 8.0 y versiones posteriores

Cuando solicitas un módulo a pedido para una App instantánea Android en Android 8.0 (nivel de API 26) y versiones posteriores, después de que una solicitud de instalación se informa como INSTALLED, debes actualizar la app con el contexto de módulo nuevo a través de una llamada a SplitInstallHelper.updateAppInfo(Context context). De lo contrario, la app no está al tanto del código y los recursos del módulo. Después de actualizar los metadatos de la app, debes cargar el contenido del módulo durante el próximo evento de subproceso principal. Para ello, invoca un Handler nuevo, como se indica a continuación:

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                // You need to perform the following only for Android Instant Apps
                // running on Android 8.0 (API level 26) and higher.
                if (BuildCompat.isAtLeastO()) {
                    // Updates the app’s context with the code and resources of the
                    // installed module.
                    SplitInstallHelper.updateAppInfo(context)
                    Handler().post {
                        // Loads contents from the module using AssetManager
                        val am = context.assets
                        ...
                    }
                }
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
            // You need to perform the following only for Android Instant Apps
            // running on Android 8.0 (API level 26) and higher.
            if (BuildCompat.isAtLeastO()) {
                // Updates the app’s context with the code and resources of the
                // installed module.
                SplitInstallHelper.updateAppInfo(context);
                new Handler().post(new Runnable() {
                    @Override public void run() {
                        // Loads contents from the module using AssetManager
                        AssetManager am = context.getAssets();
                        ...
                    }
                });
            }
        }
    }
}

Cómo cargar bibliotecas C/C++

Si quieres cargar bibliotecas C/C++ desde un módulo que el dispositivo ya descargó en una app instantánea, usa SplitInstallHelper.loadLibrary(Context context, String libName), como se muestra a continuación:

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.INSTALLED -> {
                // Updates the app’s context as soon as a module is installed.
                val newContext = context.createPackageContext(context.packageName, 0)
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”)
                ...
            }
        }
    }
}

Java

public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.INSTALLED:
                // Updates the app’s context as soon as a module is installed.
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”);
                ...
        }
    }
}

Limitaciones conocidas

  • No es posible usar WebView de Android en una actividad que accede a recursos o elementos desde un módulo opcional. Esto se debe a una incompatibilidad entre WebView y SplitCompat en el nivel de API 28 y versiones anteriores de Android.
  • No puedes almacenar en caché los objetos ApplicationInfo de Android, su contenido ni objetos que los incluyan dentro de tu app. Siempre debes recuperar estos objetos según sea necesario desde el contexto de una app. Si almacenas esos objetos en caché, podrías provocar que la app falle cuando instale un módulo de funciones.

Cómo administrar módulos instalados

Para verificar qué módulos de funciones están instalados actualmente en el dispositivo, puedes llamar a SplitInstallManager.getInstalledModules(), que muestra un Set<String> de los nombres de los módulos instalados, como se muestra a continuación.

Kotlin

val installedModules: Set<String> = splitInstallManager.installedModules

Java

Set<String> installedModules = splitInstallManager.getInstalledModules();

Cómo desinstalar módulos

Puedes pedirle al dispositivo que desinstale módulos. Para ello, invoca SplitInstallManager.deferredUninstall(List<String> moduleNames), como se muestra a continuación.

Kotlin

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))

Java

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(Arrays.asList("pictureMessages", "promotionalFilters"));

Las desinstalaciones del módulo no se realizan inmediatamente. Es decir, el dispositivo los desinstala en el segundo plano según sea necesario para ahorrar espacio de almacenamiento. Para confirmar que el dispositivo haya quitado un módulo, invoca SplitInstallManager.getInstalledModules() y también inspecciona el resultado, como se describe en la sección anterior.

Cómo descargar recursos de idiomas adicionales

Con los paquetes de aplicaciones, los dispositivos solo descargan el código y los recursos necesarios a fin de ejecutar tu app. Por lo tanto, para los recursos de idioma, el dispositivo de un usuario solo descarga los recursos de idioma de tu app que coinciden con uno o más idiomas seleccionados actualmente en la configuración del dispositivo.

Si quieres que tu app tenga acceso a recursos de idiomas adicionales, por ejemplo, para implementar un selector de idiomas en la app, puedes usar la biblioteca de Play Core a fin de descargarlos a pedido. El proceso es similar al de descargar un módulo de funciones, como se muestra a continuación.

Kotlin

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply()
...

// Creates a request to download and install additional language resources.
val request = SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build()

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request)

Java

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply();
...

// Creates a request to download and install additional language resources.
SplitInstallRequest request =
    SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build();

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request);

La solicitud se maneja como si fuese una solicitud de un módulo de funciones. Es decir, puedes supervisar el estado de la solicitud como lo harías normalmente.

Si tu app no requiere los recursos de idiomas adicionales inmediatamente, puedes postergar la instalación de la app para cuando esté en el segundo plano, como se muestra a continuación.

Kotlin

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

Cómo acceder a los recursos de idiomas descargados

Para obtener acceso a los recursos de idiomas descargados, tu app necesita ejecutar el método SplitCompat.installActivity() dentro del método attachBaseContext() de cada actividad que requiera acceso a esos recursos, como se muestra a continuación.

Kotlin

override fun attachBaseContext(base: Context) {
  super.attachBaseContext(base)
  SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  super.attachBaseContext(base);
  SplitCompat.installActivity(this);
}

Para cada actividad en la que quieres usar recursos de idiomas que descargó tu app, actualiza el contexto de la base y define una configuración regional nueva a través de su Configuration:

Kotlin

override fun attachBaseContext(base: Context) {
  val configuration = Configuration()
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
  val context = base.createConfigurationContext(configuration)
  super.attachBaseContext(context)
  SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  Configuration configuration = new Configuration();
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
  Context context = base.createConfigurationContext(configuration);
  super.attachBaseContext(context);
  SplitCompat.install(this);
}

Para que estos cambios surtan efecto, tienes que volver a crear tu actividad después de que un idioma nuevo se instale y esté listo para usar. Puedes usar el método Activity#recreate().

Kotlin

when (state.status()) {
  SplitInstallSessionStatus.INSTALLED -> {
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate()
  }
  ...
}

Java

switch (state.status()) {
  case SplitInstallSessionStatus.INSTALLED:
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate();
  ...
}

Cómo desinstalar recursos de idiomas adicionales

Del mismo modo que los módulos de funciones, puedes desinstalar recursos adicionales en cualquier momento. Antes de solicitar una desinstalación, determina qué idiomas están instalados actualmente, de la siguiente manera.

Kotlin

val installedLanguages: Set<String> = splitInstallManager.installedLanguages

Java

Set<String> installedLanguages = splitInstallManager.getInstalledLanguages();

Luego, puedes decidir qué idiomas desinstalar con el método deferredLanguageUninstall(), como se muestra a continuación.

Kotlin

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

Cómo realizar pruebas locales de instalaciones de módulos

La biblioteca de Play Core te permite probar de forma local la capacidad de tu app para hacer lo siguiente, sin necesidad de conectarte a Play Store:

En esta página, se describe cómo implementar los APK divididos de tu app en el dispositivo de prueba para que Play Core use automáticamente esos APK a fin de simular la solicitud, descarga e instalación de módulos desde Play Store.

Si bien no necesitas realizar ningún cambio en la lógica de tu app, deberás cumplir con los siguientes requisitos:

Cómo compilar un conjunto de APK

Si aún no lo hiciste, compila los APK divididos de tu app de la siguiente manera:

  1. Crea un paquete de aplicación para tu app con uno de los siguientes métodos:
  2. Usa bundletool a fin de generar un conjunto de APK para todas las configuraciones del dispositivo con el siguiente comando:

    bundletool build-apks --local-testing
      --bundle my_app.aab
      --output my_app.apks
    

La marca --local-testing incluye metadatos en los manifiestos de tus APK que permiten que la biblioteca de Play Core sepa usar los APK de división local para probar la instalación de los módulos de funciones sin necesidad de conectarse a Play Store.

Cómo implementar tu app en el dispositivo

Después de compilar un conjunto de APK con la marca --local-testing, usa bundletool para instalar la versión base de tu app y transferir los APK adicionales al almacenamiento local de tu dispositivo. Puedes realizar ambas acciones con el siguiente comando:

bundletool install-apks --apks my_app.apks

Ahora, cuando inicias tu app y completas el flujo de usuarios para descargar e instalar un módulo de funciones, la biblioteca de Play Core usa los APK que bundletool transfirió al almacenamiento local del dispositivo.

Cómo simular un error de red

Para simular las instalaciones de módulos desde Play Store, la biblioteca de Play Core usa una alternativa a SplitInstallManager, llamada FakeSplitInstallManager, para solicitar el módulo. Cuando usas bundletool con la marca --local-testing para compilar un conjunto de APK y, luego, implementarlos en el dispositivo de prueba, se incluyen metadatos que le indican a la biblioteca de Play Core que debe cambiar automáticamente las llamadas a la API de tu app a fin de invocar FakeSplitInstallManager, en lugar de SplitInstallManager.

FakeSplitInstallManager incluye una marca booleana que puedes habilitar para simular un error de red la próxima vez que tu app solicite instalar un módulo. Para acceder a FakeSplitInstallManager en tus pruebas, puedes obtener una instancia usando FakeSplitInstallManagerFactory, como se muestra a continuación:

Kotlin

// Creates an instance of FakeSplitInstallManager with the app's context.
val fakeSplitInstallManager = FakeSplitInstallManagerFactory.create(context)
// Tells Play Core Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true)

Java

// Creates an instance of FakeSplitInstallManager with the app's context.
FakeSplitInstallManager fakeSplitInstallManager =
    FakeSplitInstallManagerFactory.create(context);
// Tells Play Core Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true);