Konfigurationsarchitektur: Build-Zeit, Laufzeit und Remote sauber trennen
Eine App braucht drei Konfigurationsebenen: Build-Zeit, Laufzeit und Remote. Du lernst, wie du sie strukturiert in Android-Projekten einsetzt.
Eine App hat selten nur eine einzige Laufzeitumgebung. Debug-Build, Staging-Server, Produktionsfreigabe, A/B-Test — überall gelten andere Werte, andere Schalter, andere Verhaltensweisen. Configuration Architecture gibt dir ein mentales Modell, um diese Varianz strukturiert zu verwalten, ohne deinen Code mit hartcodierten Strings zu durchsetzen.
Was ist das?
Configuration Architecture bezeichnet die bewusste Trennung von Konfigurationswissen nach Zeitpunkt und Ursprung. Drei Ebenen stehen im Zentrum:
- Build-Zeit-Konfiguration — Werte, die beim Kompilieren feststehen: App-ID, Versionsnummer, Build-Typ (debug/release) oder ein API-Endpunkt, der sich je nach Product Flavor unterscheidet.
- Laufzeit-Konfiguration — Einstellungen, die zur Laufzeit aus lokalem Speicher, DataStore oder Präferenzen geladen werden, zum Beispiel Nutzereinstellungen oder dynamische Schwellenwerte.
- Remote-Konfiguration — Werte, die ein Server dynamisch liefert, typischerweise über Firebase Remote Config. Damit kannst du Features ein- und ausschalten, ohne eine neue App-Version auszuliefern.
Diese drei Ebenen klar zu trennen verhindert, dass dein Quellcode zu einer einzigen hartcodierten Wahrheit wird, die bei jeder Umgebungsänderung bricht. Gleichzeitig macht die Trennung jeden Wert nachvollziehbar: Du weißt sofort, ob ein Schalter beim Build gesetzt, lokal gespeichert oder remote gesteuert ist.
Wie funktioniert es?
Build-Zeit: BuildConfig und Product Flavors
Gradle generiert beim Build automatisch die Klasse BuildConfig. Jeder Wert, den du in build.gradle.kts mit buildConfigField(...) definierst, landet dort als Kompilierzeit-Konstante. Du aktivierst die Klasse explizit:
android {
buildFeatures { buildConfig = true }
}
Mit Product Flavors führst du Konfigurationsdimensionen ein — etwa staging und production — und vergibst je Flavor eigene Werte für BASE_URL oder Feature-Schalter. Wechselst du den Build-Typ, wechseln die Konstanten automatisch mit.
Laufzeit: Repositories und Dependency Injection
Laufzeitkonfiguration sollte niemals direkt aus einer Activity oder einem Composable abgerufen werden. Stattdessen modellierst du ein ConfigRepository, das die Quelle (SharedPreferences, DataStore) kapselt. Per Dependency Injection — in der Regel Hilt — bekommt das ViewModel den Repository injiziert und liest Werte nur dort ab. Die UI kennt weder Quelle noch Schlüsselnamen.
Remote: Firebase Remote Config und Feature-Flags
Firebase Remote Config synchronisiert zur Laufzeit einen Schlüssel-Wert-Store vom Server. Du definierst Standardwerte lokal für den Offline-Fall, rufst fetchAndActivate() auf und liest danach über remoteConfig.getBoolean(key) oder getString(key) aus.
Feature-Flags folgen demselben Prinzip, profitieren aber zusätzlich von einem Interface:
interface FeatureFlags {
val newOnboardingEnabled: Boolean
}
class RemoteFeatureFlags @Inject constructor(
private val remoteConfig: FirebaseRemoteConfig
) : FeatureFlags {
override val newOnboardingEnabled: Boolean
get() = remoteConfig.getBoolean("feature_new_onboarding_enabled")
}
Das Interface lässt sich im Unit-Test problemlos durch einen Fake ersetzen, ohne Firebase initialisieren zu müssen.
In der Praxis
Setup mit BuildConfig und Product Flavors
// build.gradle.kts (app)
android {
buildFeatures { buildConfig = true }
defaultConfig {
buildConfigField("String", "API_BASE_URL", "\"https://api.staging.example.com\"")
}
flavorDimensions += "environment"
productFlavors {
create("staging") {
dimension = "environment"
// übernimmt den defaultConfig-Wert
}
create("production") {
dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
}
}
}
Im Code greifst du ausschließlich auf BuildConfig.API_BASE_URL zu — nie auf einen hartcodierten String. Wechselst du in Android Studio die Run-Konfiguration auf den production-Flavor, zeigt deine App automatisch auf den Produktions-Endpunkt.
Remote Config initialisierst du einmalig beim App-Start, zum Beispiel in einer Hilt-Modul-Klasse:
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
// Flags sind jetzt aktiv, UI kann neu gerendert werden
}
}
Häufige Stolperfalle 1: API-Keys in BuildConfig
BuildConfig-Felder sind zwar aus dem Quellcode entfernt, aber im dekompilierten APK mit Standard-Tools wie jadx lesbar. Sensible Credentials — Zahlungs-Tokens, Admin-Secrets, private Keys — haben dort nichts zu suchen. Leite solche Anfragen über einen Backend-Proxy, der den Schlüssel serverseitig hält und nur autorisierte App-Anfragen durchlässt.
Häufige Stolperfalle 2: Kaltstart und Remote-Config-Timing
Remote Config liefert erst nach dem ersten erfolgreichen fetchAndActivate()-Aufruf die Serverwerte. Werden Feature-Flags direkt beim Kaltstart abgefragt — etwa um zu entscheiden, welchen Onboarding-Flow du zeigst — greifst du auf die lokalen Defaults zurück. Das ist kein Fehler, sondern Absicht: Plane bewusst sinnvolle Defaults und dokumentiere, was beim ersten Start gilt, damit das Produktteam nicht überrascht wird.
Fazit
Configuration Architecture ist kein optionales Architekturprinzip — sie verhindert, dass deine App bei einem Umgebungswechsel oder einem schnellen Feature-Rollout auseinanderfällt. Prüfe in deiner aktuellen App, wo noch hartcodierte Strings stecken, die eigentlich in BuildConfig oder Remote Config gehören. Schreib anschließend einen Unit-Test gegen ein FeatureFlags-Fake-Interface und beobachte, wie klar sich Konfigurationsentscheidungen von UI-Logik trennen lassen. Dieser Refactoring-Schritt ist oft kleiner als befürchtet — und macht den Unterschied zwischen einer App, die du blind deployen kannst, und einer, bei der jeder Release ein manueller Suchprozess in verstreuten Konstanten ist.