Android Coden
Android 4 min lesen

Data-Module: Datenzugriff in Android sauber kapseln

Data-Module isolieren Datenzugriff und bieten Features saubere Interfaces. Du lernst, warum diese Trennung Testbarkeit und Wartbarkeit entscheidend verbessert.

Moderne Android-Apps bestehen aus dutzenden Klassen, die Netzwerk, Datenbank und Geschäftslogik miteinander verknüpfen. Ohne klare Grenzen entsteht schnell Code, in dem ein ViewModel direkt Retrofit-Calls ausführt oder Room-DAOs direkt in Activities aufgerufen werden. Data-Module lösen dieses Problem: Sie fassen den gesamten Datenzugriff in einem eigenen Gradle-Modul zusammen und stellen der restlichen App eine stabile, gut testbare Schnittstelle bereit.

Was ist das?

Ein Data-Modul ist ein eigenständiges Gradle-Modul, das ausschließlich für den Zugriff auf Daten zuständig ist. Die offizielle Android-Architektur bezeichnet diesen Bereich als Data Layer. Er besteht aus zwei Typen von Klassen:

  • Repositories koordinieren mehrere Datenquellen und liefern der App ein einheitliches Datenmodell. Sie sind die einzige Schnittstelle, über die Feature-Module an Daten gelangen.
  • Data Sources kapseln den Zugriff auf je eine konkrete Quelle: eine Remote-API via Retrofit, eine lokale Datenbank via Room oder persistente Einstellungen via DataStore.

Das Entscheidende ist die Richtung der Abhängigkeiten: Ein Feature-Modul wie :feature:profile darf das Modul :data:user nutzen – aber :data:user darf niemals zurück auf ein Feature-Modul oder auf UI-Code zeigen. Diese Einbahnstraße schützt die Datenschicht davor, mit Präsentations-Logik zu vermengt zu werden.

Wie funktioniert es?

Der Kern des Musters ist ein Repository-Interface, das beschreibt, was die Datenschicht kann, ohne zu verraten, wie sie es tut. Feature-Module kennen nur das Interface, niemals die Implementierung.

interface UserRepository {
    suspend fun getUser(id: String): User
    fun observeUsers(): Flow<List<User>>
}

Die Implementierung entscheidet intern über Cache-Strategie und Datenquelle:

class DefaultUserRepository(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource,
) : UserRepository {

    override suspend fun getUser(id: String): User {
        val cached = localDataSource.getUser(id)
        if (cached != null) return cached
        return remoteDataSource.fetchUser(id).also {
            localDataSource.saveUser(it)
        }
    }

    override fun observeUsers(): Flow<List<User>> =
        localDataSource.observeAll()
}

Das ViewModel kennt nur UserRepository – es fragt Daten an, ohne zu wissen, ob sie aus dem Netzwerk oder dem Cache kommen. Für Dependency Injection mit Hilt bindet ein Modul die Implementierung:

@Module
@InstallIn(SingletonComponent::class)
abstract class UserDataModule {
    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: DefaultUserRepository
    ): UserRepository
}

Typische Modulstruktur

:app
:feature:profile      → hängt ab von :data:user
:data:user            → UserRepository, DefaultUserRepository,
                        UserRemoteDataSource, UserLocalDataSource
:core:network         → Retrofit, OkHttp
:core:database        → Room-Datenbank, DAOs

Diese Aufteilung hält Compile-Zeiten kurz: Ändert sich nur die Netzwerkschicht in :core:network, muss :feature:profile nur dann neu kompilieren, wenn die Schnittstelle in :data:user sich ändert.

In der Praxis

Der häufigste Fehler: Repository-Klassen ohne Interface anlegen. Sobald DefaultUserRepository direkt im ViewModel verwendet wird, lässt sich die Klasse in Tests kaum ersetzen – echte Room-Datenbanken und Retrofit-Clients machen Unit-Tests langsam und fragil.

Entscheidungsregel: Beginne immer mit dem Interface. Schreibe zuerst einen Test gegen ein FakeUserRepository, dann implementiere DefaultUserRepository. Diese Reihenfolge zwingt dich, das Interface sauber zu halten, bevor du dich in Implementierungsdetails verlierst.

class FakeUserRepository : UserRepository {
    private val users = mutableListOf<User>()

    override suspend fun getUser(id: String): User =
        users.first { it.id == id }

    override fun observeUsers(): Flow<List<User>> =
        flowOf(users)

    fun addUser(user: User) { users.add(user) }
}

Mit diesem Fake lässt sich ein ViewModel-Test in Millisekunden ausführen – kein Netzwerk, keine Datenbank, keine Flakiness.

Zweite Stolperfalle: Datentransformationen im ViewModel statt im Repository. Die Frage „Hole Daten remote, wenn Cache älter als fünf Minuten” ist Datenschicht-Logik. Landet sie im ViewModel, ist sie schwer isoliert zu testen und kann nicht von mehreren Features wiederverwendet werden. Repositories sind der richtige Ort für diese Entscheidungen.

Dritte Stolperfalle: Ein Repository für die gesamte App anlegen. Besser ist es, fachlich zuzuschneiden: UserRepository, ArticleRepository, SettingsRepository. So bleiben Klassen klein, Tests fokussiert und Änderungen lokal begrenzt.

Fazit

Data-Module sind der Schritt, der eine wachsende App aus dem Chaos in eine klare Architektur überführt. Repositories schaffen den Vertrag, Data Sources kapseln die Details, und die strikte Abhängigkeitsrichtung verhindert, dass Datenzugriffscode in ViewModels oder Composables versickert. Um das Gelernte zu festigen: Öffne ein bestehendes Projekt und suche nach Retrofit- oder Room-Aufrufen außerhalb eines Repositories. Extrahiere mindestens einen davon hinter ein Interface, schreibe einen Test mit einem Fake und beobachte, wie der Code sofort testbarer und klarer wird. Wer diesen Refactoring-Schritt einmal konsequent durchgeführt hat, versteht intuitiv, warum Data-Module zum Fundament jeder professionellen Android-Architektur gehören.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.