¿Cómo estructurás los feature flags en una app Android grande y modularizada?
Conectar cada módulo de feature directo a Firebase Remote Config acopla toda tu app a un proveedor — un pequeño módulo de API de flags es lo que la mantiene intercambiable.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo estructurás los feature flags en una app Android grande y modularizada?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Una app modularizada normalmente tiene un :feature:checkout, un :feature:search, un :feature:rewards, cada uno compilado y testeado de forma aislada, más un módulo :app que los ensambla. Los feature flags atraviesan todos esos módulos por igual — un kill switch para checkout, un rollout gradual para un nuevo ranking de búsqueda, un A/B test sobre el copy de rewards — y el movimiento ingenuo es agregar firebase-config como dependencia a cada feature module y llamar a FirebaseRemoteConfig.getInstance().getBoolean(...) donde haga falta. Eso funciona el primer día y se convierte en un problema real al sexto mes: ahora cada feature module depende de un SDK de un proveedor específico, los tests unitarios de cada feature module necesitan fakear o mockear Firebase en vez de una interfaz chiquita, y cambiar de proveedor — o simplemente probar un kill switch sin red — significa tocar una docena de módulos en vez de uno solo.
#Dos tipos de flags, y no van en el mismo lugar
Vale la pena separar los flags de compile-time de los de runtime antes de diseñar nada. Un flag de compile-time — habilitar una feature entera sólo en un build interno de QA — va en variantes de build de Gradle y campos BuildConfig; queda horneado en el binario y nunca cambia sin un build nuevo. Un flag de runtime — un kill switch, un rollout por porcentaje, una variante de A/B test — se tiene que chequear con la app corriendo y tiene que poder cambiar sin publicar un APK nuevo. La arquitectura de acá abajo es para el segundo tipo.
#El módulo de API del que dependen todos los features
// :core:feature-flags — Kotlin puro, cero dependencias de proveedores.enum class FeatureFlag(val key: String, val defaultValue: Boolean) {NEW_CHECKOUT_FLOW("new_checkout_flow", defaultValue = false),ONE_CLICK_REORDER("one_click_reorder", defaultValue = false),DISABLE_SOCIAL_LOGIN("disable_social_login", defaultValue = false), // kill switch}interface FeatureFlagProvider {fun isEnabled(flag: FeatureFlag): Boolean}
Cada feature module depende de este módulo y de nada más relacionado a flags. :feature:checkout nunca importa com.google.firebase.*, nunca mockea FirebaseRemoteConfig en un test, y no le importa si la implementación real es Remote Config, LaunchDarkly, o un archivo de configuración — sólo conoce FeatureFlagProvider.
#El único módulo autorizado a saber que Firebase existe
// :core:feature-flags-implclass RemoteFeatureFlagProvider @Inject constructor(private val remoteConfig: FirebaseRemoteConfig,) : FeatureFlagProvider {override fun isEnabled(flag: FeatureFlag): Boolean =// getBoolean cae de vuelta a los defaults empaquetados en el APK vía// remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults),// así que un flag resuelve algo razonable antes del primer fetch.remoteConfig.getBoolean(flag.key)}class DebugFeatureFlagProvider @Inject constructor(private val overrides: DataStore<Preferences>,) : FeatureFlagProvider {override fun isEnabled(flag: FeatureFlag): Boolean = runBlocking {val key = booleanPreferencesKey(flag.key)overrides.data.first()[key] ?: flag.defaultValue}suspend fun setOverride(flag: FeatureFlag, enabled: Boolean) {overrides.edit { it[booleanPreferencesKey(flag.key)] = enabled }}}
// :app — la raíz de composición. Los feature modules nunca ven este archivo.@Module@InstallIn(SingletonComponent::class)abstract class FeatureFlagModule {@Bindsabstract fun bindFeatureFlagProvider(impl: RemoteFeatureFlagProvider, // se cambia por DebugFeatureFlagProvider en un módulo de DI sólo para debug): FeatureFlagProvider}// Uso dentro de un feature module — depende sólo de :core:feature-flags.class CheckoutViewModel @Inject constructor(private val featureFlags: FeatureFlagProvider,) : ViewModel() {val useNewCheckoutFlow: Booleanget() = featureFlags.isEnabled(FeatureFlag.NEW_CHECKOUT_FLOW)}
#Cómo se ve el grafo de módulos
Fijate en las flechas de dependencia: los feature modules apuntan al módulo de API, el módulo de implementación también apunta al de API (porque implementa la interfaz) y a Firebase, y sólo :app depende de la implementación. Ningún feature module tiene un camino en el grafo que llegue directo a Firebase — es una garantía a nivel de Gradle, no sólo una convención que la gente tiene que acordarse de respetar.
#La parte que los equipos se olvidan: borrar los flags
El testing se beneficia de la misma inversión: un test unitario de CheckoutViewModel construye un FakeFeatureFlagProvider trivial que devuelve un mapa fijo de booleanos, sin SDK de Firebase, sin red, y sin flakiness atada a estado remoto. Ése es un segundo beneficio, independiente, de la misma decisión arquitectónica — desacoplarse del proveedor le da flexibilidad a producción, y también desacopla a los tests del proveedor para que sean determinísticos.
🧠 Check your understanding
0/4 · 0/4 answered1. ¿Por qué los feature modules deberían depender de una interfaz FeatureFlagProvider en vez de Firebase Remote Config directamente?
2. ¿Por qué mandar valores por defecto locales para cada flag dentro del APK en vez de depender sólo del fetch remoto?
3. ¿Qué es la 'deuda de flags'?
4. ¿Cuál es la ventaja principal de un DebugFeatureFlagProvider respaldado por DataStore local en builds de debug?