tutoriales.com

¡Despide la Fragmentación! Construyendo una Arquitectura Modular Robusta en Android con Dynamic Feature Modules

Este tutorial te guiará a través del proceso de diseño e implementación de una arquitectura modular para tu aplicación Android utilizando Dynamic Feature Modules. Descubrirás cómo mejorar la escalabilidad, reducir el tamaño inicial del APK y optimizar el desarrollo de grandes proyectos. Aprenderás desde la configuración inicial hasta la comunicación entre módulos, haciendo tu app más robusta y fácil de mantener.

Intermedio20 min de lectura10 views
Reportar error

🚀 Introducción a la Arquitectura Modular y Dynamic Feature Modules

En el vertiginoso mundo del desarrollo de aplicaciones Android, la escalabilidad, la mantenibilidad y el rendimiento son cruciales. A medida que las aplicaciones crecen en complejidad, un diseño monolítico puede convertirse rápidamente en una pesadilla, dificultando la colaboración, ralentizando los tiempos de compilación y aumentando el tamaño del APK.

Aquí es donde entra en juego la arquitectura modular. Dividir tu aplicación en módulos más pequeños e independientes no solo organiza tu código, sino que también abre la puerta a funcionalidades avanzadas ofrecidas por Google Play, como la entrega condicional o bajo demanda.

En este tutorial, exploraremos a fondo cómo implementar una arquitectura modular robusta en Android utilizando Dynamic Feature Modules (DFM). Los DFM no son solo una forma de estructurar tu código, sino una poderosa herramienta para optimizar la experiencia del usuario y la eficiencia de tu desarrollo.

¿Por qué Modularizar tu App Android? 🤔

Modularizar tu aplicación Android ofrece una serie de beneficios significativos:

  • Reducción del tamaño del APK: Los usuarios solo descargan las características que necesitan, lo que resulta en un APK base más pequeño y descargas más rápidas.
  • Mejora de los tiempos de compilación: Compilar módulos individuales es más rápido que compilar un monolito completo.
  • Desarrollo en paralelo: Equipos grandes pueden trabajar en diferentes módulos simultáneamente con menos conflictos.
  • Reutilización de código: Los módulos pueden ser reutilizados en diferentes partes de la aplicación o incluso en otras aplicaciones.
  • Mantenibilidad: Aislar funcionalidades en módulos facilita la depuración y la implementación de nuevas características.
  • Entrega bajo demanda: Permite a los usuarios descargar características solo cuando las necesitan, liberando espacio en el dispositivo y mejorando la primera impresión.
💡 Consejo: Piensa en cada módulo como una micro-aplicación dentro de tu aplicación principal. Esto te ayudará a definir sus responsabilidades y fronteras.

¿Qué son los Dynamic Feature Modules (DFM)? ✨

Los Dynamic Feature Modules son una característica de las Android App Bundles que permiten a tu aplicación descargar características específicas a demanda. Esto significa que ciertas partes de tu aplicación no se incluyen en el APK base inicial, sino que se descargan cuando el usuario las requiere.

Esto es ideal para funcionalidades que no son esenciales para la experiencia principal o que solo usa un subconjunto de usuarios (por ejemplo, tutoriales avanzados, funciones premium, galerías de imágenes voluminosas, etc.).

🔥 Importante: Los Dynamic Feature Modules solo funcionan si tu aplicación se distribuye a través de un Android App Bundle (AAB). Si todavía usas APKs tradicionales, no podrás aprovechar esta funcionalidad.

🛠️ Configuración del Proyecto para DFM

Antes de sumergirnos en la creación de módulos, necesitamos configurar nuestro proyecto Android para que sea compatible con App Bundles y DFM.

Paso 1: Convertir tu proyecto a Android App Bundle (Si aún no lo has hecho) 📦

Si tu proyecto es relativamente nuevo, probablemente ya esté configurado para App Bundles. Sin embargo, es bueno verificar. Abre tu build.gradle a nivel de aplicación (module-level) y asegúrate de que el plugin sea com.android.application y no com.android.library (para la app base) o com.android.dynamic-feature (para los módulos de características).

Para generar un App Bundle, simplemente selecciona Build > Generate Signed Bundle / APK... > Android App Bundle en Android Studio.

Paso 2: Estructura Básica de un Proyecto Modular 🏗️

Un proyecto modular típicamente consta de los siguientes tipos de módulos:

  • app (Base Application Module): Es el módulo principal que los usuarios instalan primero. Contiene el código base de la aplicación, el manifiesto principal y las dependencias compartidas por todos los módulos. Depende de los módulos de características, pero estos no dependen de él.
  • feature (Dynamic Feature Modules): Estos módulos contienen funcionalidades específicas que pueden ser entregadas condicionalmente o bajo demanda. Son autónomos y tienen sus propios recursos y clases.
  • library (Library Modules): Módulos que encapsulan código común, utilidades, componentes de UI reutilizables o capas de datos que son utilizados por uno o más módulos de características y/o el módulo base. No son instalables por sí mismos.
app featureA featureB core_library

Paso 3: Crear un Nuevo Dynamic Feature Module ➕

Crear un DFM es sencillo desde Android Studio:

  1. Ve a File > New > New Module....
  2. En el asistente, selecciona Dynamic Feature Module y haz clic en Next.
  3. Asigna un Module name (ej. feature_profile).
  4. Asigna un Package name (ej. com.yourcompany.app.profile).
  5. Selecciona el Base application module (generalmente app).
  6. Marca las opciones si las necesitas (ej. Enable on-demand install, Fusing).
  7. Haz clic en Finish.

Android Studio creará una nueva carpeta para el módulo y añadirá las configuraciones necesarias.

Archivo build.gradle del Módulo Base (app)

Tu build.gradle del módulo app ahora deberá incluir el módulo de característica en su bloque android:

// app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    // ... otras configuraciones
    dynamicFeatures = [":feature_profile", ":feature_settings"]
}

dependencies {
    // Dependencias comunes de la app base
    implementation project(':core:ui') // Ejemplo de módulo de librería
    implementation 'androidx.core:core-ktx:1.12.0'
    // ...
}

Archivo build.gradle del Módulo de Característica (feature_profile)

El build.gradle del nuevo módulo de característica tendrá el plugin com.android.dynamic-feature y una dependencia al módulo app:

// feature_profile/build.gradle
plugins {
    id 'com.android.dynamic-feature'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 34

    defaultConfig {
        minSdk 21
        // ... otras configuraciones
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation project(':app') // Dependencia al módulo base
    implementation project(':core:navigation') // Ejemplo de módulo de librería
    
    implementation 'androidx.appcompat:appcompat:1.6.1'
    // ... dependencias específicas de esta característica
}
📌 Nota: Los módulos de característica *siempre* tienen una dependencia de `implementation project(':app')`. Esto no significa que `app` dependa de `feature_profile`, sino que `feature_profile` puede acceder a los recursos y clases públicas del módulo `app`.

🤝 Comunicación entre Módulos

Uno de los mayores desafíos en una arquitectura modular es cómo hacer que los módulos se comuniquen de manera efectiva y segura, manteniendo un bajo acoplamiento.

1. Comunicación Unidireccional: Módulo Base a Módulo de Característica

El módulo app (base) puede lanzar componentes (Activities, Fragments) definidos en un módulo de característica. Para hacer esto, necesitas conocer el nombre completo de la clase del componente.

// En el módulo 'app' o en otro módulo que dependa de 'feature_profile'

val intent = Intent().setClassName(packageName, "com.yourcompany.app.profile.ProfileActivity")
startActivity(intent)

Para que esto funcione, la ProfileActivity debe estar declarada en el AndroidManifest.xml de feature_profile.

<!-- feature_profile/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <dist:module
        dist:instant="false"
        dist:onDemand="true"
        dist:title="@string/title_profile_feature">
        <dist:fusing dist:include="true" />
    </dist:module>

    <application>
        <activity
            android:name=".ProfileActivity"
            android:exported="false" />
    </application>
</manifest>

2. Comunicación Bidireccional o Reutilización de Componentes: Interfaces y Módulos de Librería 📚

Cuando necesitas que un módulo de característica exponga una funcionalidad o que varios módulos compartan componentes, la mejor práctica es usar interfaces definidas en un módulo de librería compartido (ej. core:common, core:navigation, core:ui).

Ejemplo: Un módulo core:navigation que define interfaces para lanzar pantallas.

core:navigation/src/main/java/com/yourcompany/app/core/navigation/ProfileNavigator.kt

// Módulo core:navigation
package com.yourcompany.app.core.navigation

interface ProfileNavigator {
    fun navigateToProfile(userId: String)
}

feature_profile/src/main/java/com/yourcompany/app/profile/ProfileNavigatorImpl.kt

// Módulo feature_profile
package com.yourcompany.app.profile

import android.content.Context
import android.content.Intent
import com.yourcompany.app.core.navigation.ProfileNavigator

// Para DI, podrías usar Hilt o Koin para proporcionar esta implementación
class ProfileNavigatorImpl(private val context: Context) : ProfileNavigator {
    override fun navigateToProfile(userId: String) {
        val intent = Intent(context, ProfileActivity::class.java).apply {
            putExtra("USER_ID", userId)
        }
        context.startActivity(intent)
    }
}

Uso en el Módulo Base o en otros Módulos

Ahora, el módulo app (o cualquier otro módulo que dependa de core:navigation y tenga acceso a la implementación) puede usar ProfileNavigator sin conocer los detalles internos de feature_profile.

// En el módulo 'app' o en otro módulo que necesite navegar al perfil

// Suponiendo que tienes una instancia de ProfileNavigator (inyectada o creada)
val profileNavigator: ProfileNavigator = getProfileNavigatorInstance() // Obtén tu instancia
profileNavigator.navigateToProfile("user123")
📌 Nota: Para manejar la inyección de dependencias de estas interfaces de manera efectiva en un entorno modular, herramientas como Dagger Hilt o Koin son muy recomendables. Podrías definir módulos DI específicos para cada feature que provean sus implementaciones.

🌐 Gestión de la Instalación de Módulos Dinámicos

Una de las ventajas clave de los DFM es la posibilidad de instalarlos bajo demanda. Esto requiere el uso de la API SplitInstallManager.

1. Comprobar la Disponibilidad de un Módulo 🔍

Antes de intentar usar una característica, es buena idea verificar si ya está instalada.

import com.google.android.play.core.splitinstall.SplitInstallManagerFactory

val splitInstallManager = SplitInstallManagerFactory.create(context)
val isProfileFeatureInstalled = splitInstallManager.installedModules.contains("feature_profile")

if (isProfileFeatureInstalled) {
    // El módulo ya está instalado, puedes navegar a la actividad directamente
    val intent = Intent().setClassName(context.packageName, "com.yourcompany.app.profile.ProfileActivity")
    context.startActivity(intent)
} else {
    // El módulo no está instalado, inicia la descarga
    requestInstallProfileFeature()
}

2. Solicitar la Instalación de un Módulo Bajo Demanda ⬇️

Para instalar un módulo dinámico, debes usar SplitInstallManager.

import com.google.android.play.core.splitinstall.SplitInstallManager
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
import com.google.android.play.core.splitinstall.SplitInstallRequest
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus

fun requestInstallProfileFeature(context: Context) {
    val splitInstallManager = SplitInstallManagerFactory.create(context)
    val request = SplitInstallRequest.newBuilder()
        .addModule("feature_profile")
        .build()

    splitInstallManager.startInstall(request)
        .addOnSuccessListener { sessionId ->
            // La descarga ha comenzado con éxito
            Log.d("DFM", "Descarga iniciada para feature_profile. Session ID: $sessionId")
        }
        .addOnFailureListener { exception ->
            // Manejar errores de descarga
            Log.e("DFM", "Error al iniciar la descarga de feature_profile", exception)
        }

    // Opcional: Registrar un listener para el progreso de la descarga
    val listener = SplitInstallStateUpdatedListener {
        when (it.status()) {
            SplitInstallSessionStatus.DOWNLOADING -> {
                val totalBytes = it.totalBytesToDownload()
                val downloadedBytes = it.bytesDownloaded()
                val progress = if (totalBytes > 0) (downloadedBytes * 100 / totalBytes).toInt() else 0
                Log.d("DFM", "Descargando feature_profile: $progress%")
            }
            SplitInstallSessionStatus.INSTALLED -> {
                Log.d("DFM", "feature_profile instalado. Lanzando actividad...")
                // Una vez instalado, puedes lanzar la actividad
                val intent = Intent().setClassName(context.packageName, "com.yourcompany.app.profile.ProfileActivity")
                context.startActivity(intent)
                splitInstallManager.unregisterListener(this) // Importante desregistrar
            }
            SplitInstallSessionStatus.FAILED -> {
                Log.e("DFM", "Error en la instalación de feature_profile: ${it.errorCode()}")
                splitInstallManager.unregisterListener(this)
            }
            // ... otros estados
        }
    }
    splitInstallManager.registerListener(listener)
}
⚠️ Advertencia: Es crucial registrar y desregistrar el `SplitInstallStateUpdatedListener` correctamente para evitar fugas de memoria y asegurar que recibes los eventos de estado.

3. Eliminar Módulos Instalados (Opcional) 🗑️

Si una característica ya no es necesaria, puedes solicitar su desinstalación para liberar espacio.

fun uninstallProfileFeature(context: Context) {
    val splitInstallManager = SplitInstallManagerFactory.create(context)
    splitInstallManager.deferredUninstall(listOf("feature_profile"))
        .addOnSuccessListener { 
            Log.d("DFM", "Desinstalación de feature_profile solicitada con éxito")
        }
        .addOnFailureListener { exception ->
            Log.e("DFM", "Error al solicitar la desinstalación de feature_profile", exception)
        }
}

🎨 Consideraciones de Diseño UI y Recursos Compartidos

En una arquitectura modular, la gestión de la UI y los recursos compartidos requiere una planificación cuidadosa para evitar duplicaciones y mantener la consistencia visual.

1. Módulos de Librería para UI Común 🖼️

Crea módulos de librería (core:ui, core:components) para albergar:

  • Temas y Estilos: Define tu tema principal y estilos comunes en un módulo de UI para que todos los módulos de característica lo hereden.
  • Componentes Custom: Botones personalizados, vistas complejas, RecyclerView.Adapters genéricos.
  • Recursos Comunes: Drawables, colores, dimensiones, strings genéricas que se usan en toda la app.
// feature_profile/build.gradle
dependencies {
    implementation project(':app')
    implementation project(':core:ui') // Dependencia al módulo de UI común
    // ...
}
💡 Consejo: Usa el prefijo `core:` para tus módulos de librería para distinguirlos claramente de los módulos de característica.

2. Manejo de Nombres de Recursos 📝

Cada módulo puede tener sus propios recursos (strings, layouts, drawables). Para evitar colisiones de nombres, especialmente si los recursos tienen el mismo nombre pero diferente contenido, Android Studio automáticamente califica los recursos con el nombre del módulo. Sin embargo, es una buena práctica:

  • Prefijos: Usa prefijos para los nombres de los recursos dentro de cada módulo. Por ejemplo, profile_title, profile_item_layout en el módulo feature_profile.
  • Sobrescribir: Si un módulo de característica tiene un recurso con el mismo nombre que un recurso del módulo app, el recurso del módulo de característica no sobrescribirá el del módulo base si se refiere directamente a él. Solo se usa el recurso más cercano en la jerarquía de dependencias. Para sobreescribir, deberás manejarlo manualmente o con configuraciones avanzadas.

📈 Optimización y Buenas Prácticas

Para sacar el máximo provecho de tu arquitectura modular con DFM, considera estas optimizaciones y buenas prácticas.

1. Fusing de Módulos 융합

La opción fusing en el AndroidManifest.xml de un DFM (dist:fusing dist:include="true") determina si el módulo debe ser incluido en el APK base en dispositivos con versiones de Android anteriores a Android 5.0 (API nivel 21), donde los DFM no son soportados nativamente. Si tu minSdk es 21 o superior, esta opción tiene menos relevancia, pero sigue siendo útil si quieres que el módulo siempre se instale, incluso si es 'dinámico'.

2. Entrega Condicional 🎯

Los DFM no son solo para la entrega bajo demanda. También puedes configurar la entrega condicional en la Google Play Console para que un módulo se descargue automáticamente si cumple ciertas condiciones (país, nivel de API, características del dispositivo, etc.). Esto se configura en el bloque <dist:conditions> del AndroidManifest.xml del DFM.

<!-- feature_premium/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <dist:module dist:title="@string/title_premium_feature">
        <dist:delivery>
            <dist:install-time />
        </dist:delivery>
        <dist:conditions>
            <dist:device-feature dist:name="android.hardware.type.watch" dist:not-required="true" />
            <dist:min-api dist:value="24" />
        </dist:conditions>
    </dist:module>
    ...
</manifest>

Este módulo se instalará al mismo tiempo que la aplicación base si el dispositivo cumple las condiciones (en este caso, API 24 o superior).

3. Manejo de Dependencias y Acoplamiento 📉

  • Minimiza las dependencias: Cada módulo debe ser lo más independiente posible. Evita que los módulos de características dependan entre sí. Si necesitan compartir lógica, extráela a un módulo de librería común.
  • Inyección de Dependencias: Utiliza frameworks como Hilt/Dagger o Koin para gestionar las dependencias entre módulos de forma limpia y desacoplada. Puedes crear módulos DI específicos para cada feature.
  • API vs. Implementación: Define APIs (interfaces) en módulos de librería y sus implementaciones en módulos de característica. Esto permite que los módulos base o de librería se comuniquen con las características sin tener una dependencia directa sobre ellas.
app featureA featureB core:di core:api Flechas indican 'depende de'

4. Pruebas Unitarias e Instrumentadas 🧪

En una arquitectura modular, las pruebas son más fáciles de gestionar. Puedes ejecutar pruebas unitarias de un módulo de característica de forma aislada, lo que acelera el ciclo de feedback. Las pruebas instrumentadas pueden ejecutarse contra módulos específicos o contra la aplicación completa.


🔚 Conclusión y Pasos Siguientes

Has llegado al final de este completo tutorial sobre cómo construir una arquitectura modular robusta en Android utilizando Dynamic Feature Modules. Hemos cubierto desde la configuración básica hasta la comunicación avanzada entre módulos y las mejores prácticas para optimizar tu aplicación.

Adoptar una arquitectura modular es una inversión que rinde dividendos a largo plazo, mejorando la escalabilidad, la mantenibilidad y la eficiencia de tu equipo de desarrollo. Los Dynamic Feature Modules, en particular, te permiten ofrecer una experiencia de usuario más fluida y optimizada al reducir el tamaño inicial de la aplicación y permitir la entrega de características bajo demanda.

¡Tutorial Completado!

Recomendaciones para seguir explorando:

  • Android App Bundles: Profundiza en cómo funcionan los App Bundles y cómo optimizan la distribución de tu app.
  • Inyección de Dependencias: Explora Dagger Hilt o Koin para gestionar de forma robusta las dependencias en un proyecto modular.
  • Gradle Build Optimizations: Aprende a optimizar tus scripts de Gradle para reducir aún más los tiempos de compilación en proyectos grandes.
  • Modularización en Jetpack Compose: Si trabajas con Compose, explora cómo la modularización puede aplicarse de manera efectiva a los componentes composables.

¡Felicidades por dar este gran paso hacia una arquitectura Android más moderna y eficiente! 🎉

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!