wandres.dev
ANATOMÍA DE UNA APP · manifiesto, recursos y AAB

APK frente a App Bundle: entrega por dispositivo

Qué es un AAB y por qué desplazó al APK como formato de publicación: la generación de paquetes divididos por densidad, idioma y arquitectura, los módulos dinámicos con entrega condicional o a demanda, y cómo medir el tamaño real de descarga.

⏱ 18 min

Durante una década, publicar una app significaba subir un APK que contenía todo para todos: los iconos de las seis densidades, los textos de los ochenta idiomas, las librerías nativas de las cuatro arquitecturas. Cada usuario descargaba el conjunto completo y usaba una fracción. El AAB rompió ese modelo: subes un formato intermedio que no es instalable, y la tienda genera y firma en el servidor el paquete exacto que necesita cada dispositivo. Es el cambio con más impacto sobre el tamaño de descarga que ha tenido la plataforma, y trajo consigo una forma nueva de pensar la modularidad.

🎯 Al terminar esta lección sabrás
  • Entender qué contiene un AAB y por qué no es un formato instalable.
  • Comprender la generación de paquetes divididos por densidad, idioma y arquitectura.
  • Distinguir los tipos de módulo: base, de característica a demanda y de entrega condicional.
  • Medir el tamaño real de descarga y separarlo del tamaño del artefacto que subes.

Qué es un AAB y en qué se diferencia de un APK

Un AAB es un archivo de publicación, no de instalación. Contiene el código y los recursos de la app organizados por módulo y ya separados en dimensiones —código, recursos por densidad, recursos por idioma, librerías por arquitectura—, junto con metadatos que describen cómo puede trocearse. Ningún dispositivo puede instalarlo directamente: es materia prima para que la tienda fabrique paquetes.

mi-app.aab
  base/
    manifest/           manifiesto del modulo base
    dex/                codigo en DEX
    res/                recursos con sus calificadores
    lib/                librerias nativas por arquitectura
    assets/
  caracteristica_mapas/ modulo adicional con la misma estructura
  BUNDLE-METADATA/      metadatos de la herramienta de build

Cuando un usuario instala, la tienda mira el dispositivo —densidad de pantalla, idiomas configurados, arquitectura del procesador, versión del sistema— y genera un conjunto de paquetes divididos: uno base con el código común, y varios complementarios con exactamente la densidad, los idiomas y la arquitectura que hacen falta. Los firma con la clave real de la app y los entrega juntos. El sistema los instala como una única aplicación.

flowchart TD
DEV[Subes un AAB] --> PLAY[La tienda lo procesa]
PLAY --> GEN[Genera paquetes divididos]
GEN --> B[Paquete base con el codigo]
GEN --> D[Paquete de densidad concreta]
GEN --> I[Paquete de idiomas del usuario]
GEN --> A[Paquete de arquitectura del procesador]
B --> SIGN[Firma con la clave de la app]
D --> SIGN
I --> SIGN
A --> SIGN
SIGN --> INST[Instalacion en el dispositivo]

De aquí se deduce algo que sorprende la primera vez: la firma gestionada por la tienda no es opcional en este modelo, es su condición de posibilidad. Los paquetes divididos se fabrican en el servidor después de que tú subas nada, así que alguien en el servidor tiene que poder firmarlos, y ese alguien solo puede ser quien custodia la clave de la app.

La segunda consecuencia es que el paquete base y sus complementos no son apps independientes que colaboran: el sistema los trata como una instalación única, comparten proceso, identificador y clase R, y deben estar firmados con la misma clave y declarar la misma versión. Si alguno falta o desencaja, la instalación se rechaza entera. Esta cohesión es lo que permite que el código del módulo base referencie recursos que llegaron en un complemento sin enterarse de nada.

También cambia lo que puedes probar en local. Como el artefacto que subes no se instala, la única forma de ejecutar lo que recibirá un usuario es generar los paquetes divididos tú mismo con la herramienta oficial de bundles y desplegarlos como conjunto. Ese paso extra es la fuente del malentendido más común del formato: no es que el AAB esté roto cuando el dispositivo lo rechaza, es que nunca fue instalable.

ℹ️
El `APK` no desapareció, cambió de papel

Sigue siendo el formato que se instala: lo que llega al dispositivo son paquetes con esa extensión. Lo que dejó de ser es el formato de publicación en la tienda principal, obligatorio desde 2021 para apps nuevas. El APK sigue vivo y es imprescindible en tres escenarios: distribución fuera de la tienda, instalación directa para pruebas, y tiendas alternativas que no implementan la división. Para esos casos existe el APK universal, que la propia herramienta puede extraer de un AAB y que contiene todo para todos, exactamente como antes.

Los tres ejes de división y lo que ahorran

La división automática opera sobre tres dimensiones, y cada una ahorra por una razón distinta. La densidad elimina las copias de mapas de bits que ese dispositivo nunca dibujaría: cinco escalones de icono se reducen a uno. El idioma elimina las cadenas de las lenguas que el usuario no tiene configuradas, lo que en apps muy traducidas es una porción sorprendente del total. La arquitectura elimina las librerías nativas de procesadores ajenos, y ese es el ahorro más brutal en apps con componentes en C o motores de juego, donde cada arquitectura puede pesar decenas de megabytes.

📱

Por densidad

Solo los mapas de bits del escalón que la pantalla usa. Los vectores son inmunes a esto porque ya eran únicos.

🌍

Por idioma

Solo las cadenas de los idiomas del usuario. Si añade uno nuevo en ajustes, la tienda descarga el paquete que falta.

🔧

Por arquitectura

Solo las librerías nativas de ese procesador. En apps con código nativo suele ser el mayor ahorro individual.

🧩

Por módulo

Además de los tres ejes automáticos, tus propios módulos de característica, que se entregan o no según reglas que tú defines.

Este mecanismo también resolvió una limitación antigua: el techo de tamaño. Un APK subido a la tienda tenía un límite estricto que obligaba a los juegos grandes a distribuir archivos de expansión por separado, con toda la fragilidad que eso implicaba. Con AAB el límite se aplica a lo que descarga cada dispositivo, no al artefacto completo, y los archivos de expansión quedaron obsoletos.

Módulos: a demanda, condicional e instalado

Sobre la división automática se apoya la modularidad explícita. Un módulo de característica es un trozo de la app con su propio código y recursos que puede no formar parte de la instalación inicial. Su manifiesto declara el tipo de entrega, y ahí está toda la decisión.

<dist:module
    dist:instant="false"
    dist:title="@string/modulo_mapas">
    <dist:delivery>
        <dist:on-demand />
    </dist:delivery>
    <dist:fusing dist:include="true" />
</dist:module>

Pedir un módulo a demanda desde el código es una operación asíncrona con estados propios, y ese es exactamente el coste que introduce la modularidad dinámica.

val gestor = SplitInstallManagerFactory.create(context)
val peticion = SplitInstallRequest.newBuilder()
    .addModule("caracteristica_mapas")
    .build()

gestor.registerListener { estado ->
    when (estado.status()) {
        SplitInstallSessionStatus.DOWNLOADING -> mostrarProgreso(estado)
        SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> pedirConfirmacion(estado)
        SplitInstallSessionStatus.INSTALLED -> abrirPantallaDeMapas()
        SplitInstallSessionStatus.FAILED -> avisarDelError()
    }
}

gestor.startInstall(peticion)

Hay tres modos que conviene no confundir. La entrega a demanda deja el módulo fuera de la instalación y lo descarga cuando el código lo solicita explícitamente, lo que exige gestionar estados de descarga, errores y confirmación del usuario en descargas grandes. La entrega condicional lo incluye en la instalación solo si el dispositivo cumple criterios que declaras —una versión mínima del sistema, un país, una característica de hardware—. Y la entrega en tiempo de instalación lo incluye siempre, con lo que el módulo sirve únicamente para organizar el proyecto y acelerar compilaciones.

⚠️
La modularidad dinámica cuesta más de lo que parece

Un módulo a demanda no es una carpeta más: introduce un punto de fallo en tiempo de ejecución. Hay que manejar la descarga, el usuario sin conexión, el rechazo de la confirmación, el módulo que aún no está listo cuando la pantalla ya se abrió, y el hecho de que las clases de ese módulo no existen para el cargador hasta que llega. Además complica las pruebas, porque en depuración todo está presente y el camino de fallo no se ejercita. La regla razonable es reservarlo para lo que de verdad pesa y de verdad usa una minoría —un editor avanzado, un motor de realidad aumentada, contenido de un país concreto— y no trocear la app por elegancia arquitectónica, que para eso ya sirven los módulos normales de Gradle.

El tamaño real de descarga

Hay tres cifras distintas y confundirlas lleva a decisiones equivocadas. El tamaño del AAB que subes no le importa a nadie salvo a tu red. El tamaño de descarga es lo que el usuario baja: la suma de los paquetes divididos que le corresponden, ya comprimidos, y es la cifra que determina si alguien con datos limitados instala tu app o se arrepiente. El tamaño en disco tras instalar es mayor que ambos, porque incluye el código precompilado por el sistema y los datos de la app.

Para medirlo no hay que adivinar: existe una herramienta oficial que reproduce en local exactamente lo que la tienda haría, genera el conjunto de paquetes para una configuración de dispositivo concreta y calcula la descarga estimada. La consola de la tienda muestra además esa cifra por configuración una vez subes el artefacto.

Artefacto subido            AAB de 84 MB      irrelevante para el usuario
APK universal equivalente   78 MB             lo que se descargaba antes
Descarga real estimada      31 MB             base mas densidad, idioma y arquitectura
Ocupacion tras instalar      92 MB            incluye codigo precompilado por el sistema

Las tres primeras cifras explican por qué comparar el AAB con el APK antiguo no dice nada útil: son artefactos de naturaleza distinta y solo la tercera describe la experiencia de alguien instalando la app. La cuarta recuerda que la descarga no es el final del proceso, porque el sistema compila código nativo a partir del DEX y eso ocupa espacio adicional en el dispositivo.

Medir en local antes de publicar es la práctica que convierte el tamaño en algo gobernable, y hacerlo en la integración continua lo convierte en algo defendible: un umbral que falla la compilación cuando la descarga crece más de lo aceptable detiene la degradación silenciosa que sufren todas las apps con el paso de los años.

El bundle traslada la decisión de qué instalar del desarrollador al servidor

El cambio de fondo no es de formato, es de responsabilidad. Con un APK eras tú quien decidía, en tiempo de compilación, el contenido exacto que recibiría cada persona del planeta, y como no podías conocer sus dispositivos, la única decisión segura era incluirlo todo. Ese es el mismo patrón que ya viste en el sistema de recursos, solo que llevado un paso más allá: allí el dispositivo cargaba la variante correcta de un conjunto completo que ya tenía; aquí el conjunto completo ni siquiera llega al dispositivo, porque la selección ocurre antes, en el servidor, donde sí se conoce la configuración exacta del destinatario. Es resolución tardía aplicada a la distribución, y por eso exige lo que exige —que la tienda tenga la clave de firma— y por eso paga lo que paga, que en apps con código nativo y muchas traducciones suele estar entre el treinta y el sesenta por ciento del tamaño de descarga. Hay una lección más amplia debajo, y vale para cualquier plataforma: cada vez que un sistema mueve una decisión hacia el momento en que hay más información disponible, gana adaptación y pierde previsibilidad. El coste real del AAB es precisamente ese —ya no sabes con exactitud qué binario corre en un dispositivo concreto, y depurar un fallo específico de una arquitectura o un idioma exige reconstruir localmente el paquete que esa persona recibió—. Aceptar ese coste conscientemente, y saber generar en local el paquete exacto que se instaló, es lo que separa a quien publica de quien además puede diagnosticar lo que publicó.

⚔️ Mide el ahorro con tus propias manos
  1. Genera un AAB de tu app y anota su tamaño; genera después el APK universal equivalente y compáralos.
  2. Con la herramienta oficial de bundles, produce el conjunto de paquetes para una configuración concreta de dispositivo.
  3. Calcula el tamaño de descarga estimado para esa configuración y contrástalo con el APK universal.
  4. Repite el cálculo para un dispositivo con otra arquitectura y otro idioma, y explica de dónde sale la diferencia.
  5. Extrae un módulo pesado a una característica a demanda y vuelve a medir la descarga inicial.