Android Coden
Android 7 min lesen

Factory Pattern in Android

Factorys bündeln Objekterzeugung. So bleibt Android-Code besser konfigurierbar, testbar und entkoppelt.

Das Factory Pattern hilft dir, Objekte an einer zentralen Stelle zu erzeugen, wenn ihre Konstruktion mehr Wissen braucht als der eigentliche Feature-Code haben sollte. In Android betrifft das oft Repositories, ViewModels, API-Clients, Parser, Konfigurationen oder Klassen, die je nach Build-Variante, Nutzerstatus oder Testumgebung anders zusammengesetzt werden.

Was ist das?

Das Factory Pattern ist ein Erzeugungsmuster. Eine Factory ist eine Klasse, Funktion oder ein Objekt, das andere Objekte baut. Der aufrufende Code sagt nicht mehr: „Ich kenne alle Konstruktoren, alle Parameter und alle Sonderfälle.“ Stattdessen sagt er: „Gib mir eine passende Instanz für diesen Zweck.“

Das klingt zunächst nach einer kleinen Umleitung. Der Nutzen entsteht aber genau dort, wo Konstruktion unübersichtlich wird. Ein Repository braucht vielleicht einen Netzwerk-Client, eine lokale Datenquelle, einen Mapper und einen Dispatcher für Coroutines. Ein ViewModel braucht ein Repository, einen SavedStateHandle und eventuell eine Startkonfiguration. Wenn du diese Details direkt in einem Composable, einer Activity oder einer Feature-Klasse verteilst, vermischst du zwei Aufgaben: Fachlogik und Objekterzeugung.

Das mentale Modell ist einfach: Feature-Code beschreibt, was passieren soll. Factory-Code beschreibt, womit es passieren soll. Diese Trennung unterstützt creation, configuration und decoupling. Creation bedeutet: Die Factory übernimmt den Bau. Configuration bedeutet: Sie setzt Parameter, Modi und Varianten zusammen. Decoupling bedeutet: Der nutzende Code muss weniger über konkrete Klassen wissen.

In moderner Android-Entwicklung passt das gut zur Architektur-Empfehlung, Verantwortlichkeiten klar zu trennen. UI-Code in Compose sollte Zustände darstellen und Ereignisse weitergeben. ViewModels koordinieren UI-nahe Logik. Repositories kapseln Datenzugriff. Eine Factory sitzt nicht als neue Schicht über allem, sondern hilft an den Stellen, an denen Objekte sauber zusammengesetzt werden müssen.

Wie funktioniert es?

Eine Factory kann sehr klein sein. In Kotlin reicht oft eine Funktion:

fun createUserRepository(api: UserApi, database: AppDatabase): UserRepository {
    return DefaultUserRepository(
        remoteDataSource = RemoteUserDataSource(api),
        localDataSource = LocalUserDataSource(database.userDao())
    )
}

Das ist bereits eine Factory-Funktion. Sie nimmt die nötigen Grundbausteine entgegen und liefert ein fertig nutzbares Objekt zurück. Der Aufrufer muss nicht wissen, dass es eine RemoteDataSource und eine LocalDataSource gibt. Er bekommt ein UserRepository.

In größeren Android-Projekten findest du Factorys häufig in drei Formen. Erstens als einfache Kotlin-Funktion für kleine Konstruktionen. Zweitens als Klasse, wenn Zustand oder mehrere create-Methoden nötig sind. Drittens als spezieller Factory-Typ für Framework-Integration, zum Beispiel bei ViewModelProvider.Factory, wenn ein ViewModel nicht nur einen leeren Konstruktor hat.

Wichtig ist die Richtung der Abhängigkeiten. Eine Factory darf konkrete Klassen kennen, weil ihre Aufgabe genau darin besteht, konkrete Objekte zu bauen. Der Feature-Code sollte dagegen möglichst gegen Interfaces oder stabile Typen arbeiten. So kannst du in Tests eine andere Factory, ein Fake-Repository oder eine vorbereitete Instanz verwenden.

Eine Factory ist kein Ersatz für Dependency Injection. DI-Frameworks wie Hilt erzeugen und verdrahten Abhängigkeiten automatisiert. Trotzdem bleibt das Factory-Denken nützlich: Auch wenn später ein Framework die Arbeit übernimmt, musst du verstehen, welche Objekte gebaut werden, welche Konfiguration nötig ist und wo diese Verantwortung hingehört. Für kleine Apps, Lernprojekte oder klar abgegrenzte Module ist eine manuelle Factory oft völlig ausreichend.

Der Lebenszyklus spielt in Android eine besondere Rolle. Manche Objekte dürfen kurzlebig sein, andere nicht. Ein Repository kann häufig länger leben als ein einzelnes Composable. Ein ViewModel ist an einen ViewModelStoreOwner gebunden. Ein Context darf nicht versehentlich als Activity-Context in langlebigen Singleton-Objekten gespeichert werden. Eine Factory macht solche Entscheidungen sichtbarer, weil du an einer Stelle prüfen kannst, welche Art von Context, Scope oder Dispatcher übergeben wird.

In der Praxis

Stell dir eine Compose-App vor, die Aufgaben aus einem Repository lädt. Das Repository benötigt einen API-Service und eine lokale Datenquelle. Ohne Factory landet die Konstruktion schnell an einer ungünstigen Stelle:

@Composable
fun TaskScreen() {
    val context = LocalContext.current
    val database = AppDatabase.getInstance(context)
    val api = Retrofit.Builder()
        .baseUrl("https://example.com/")
        .build()
        .create(TaskApi::class.java)

    val repository = DefaultTaskRepository(
        remote = RemoteTaskDataSource(api),
        local = LocalTaskDataSource(database.taskDao())
    )

    TaskContent(repository = repository)
}

Dieser Code hat mehrere Probleme. Das Composable baut Infrastruktur, obwohl es UI darstellen soll. Bei jeder Recompositions-Diskussion musst du prüfen, ob Objekte neu erzeugt werden. Tests werden schwerer, weil TaskScreen konkrete Netzwerk- und Datenbankdetails kennt. Außerdem ist der Code schwer zu lesen: Die eigentliche Oberfläche ist zwischen Konstruktoren versteckt.

Mit einer Factory wird die Verantwortung klarer:

interface TaskRepository {
    suspend fun loadTasks(): List<Task>
}

class DefaultTaskRepository(
    private val remote: RemoteTaskDataSource,
    private val local: LocalTaskDataSource
) : TaskRepository {
    override suspend fun loadTasks(): List<Task> {
        val cached = local.readTasks()
        if (cached.isNotEmpty()) return cached

        val fresh = remote.fetchTasks()
        local.saveTasks(fresh)
        return fresh
    }
}

class TaskRepositoryFactory(
    private val api: TaskApi,
    private val database: AppDatabase
) {
    fun create(): TaskRepository {
        return DefaultTaskRepository(
            remote = RemoteTaskDataSource(api),
            local = LocalTaskDataSource(database.taskDao())
        )
    }
}

Der UI-Code kann nun ein TaskRepository erhalten, ohne dessen Konstruktion zu kennen. In einer kleinen App könntest du die Factory im Application-Objekt oder in einem einfachen AppContainer halten. In einer größeren App würdest du eher eine DI-Lösung verwenden. Das Prinzip bleibt gleich: Objekterzeugung wird zentralisiert, damit Feature-Code nicht mit Konfigurationsdetails überladen wird.

Für ViewModels ist das besonders greifbar. Ein ViewModel mit Repository im Konstruktor kann nicht ohne Weiteres vom Framework über einen parameterlosen Konstruktor erstellt werden. Dafür gibt es eine Factory:

class TaskViewModel(
    private val repository: TaskRepository
) : ViewModel() {

    private val _state = MutableStateFlow(TaskUiState())
    val state: StateFlow<TaskUiState> = _state

    fun refresh() {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true)
            val tasks = repository.loadTasks()
            _state.value = TaskUiState(tasks = tasks, isLoading = false)
        }
    }
}

class TaskViewModelFactory(
    private val repository: TaskRepository
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(TaskViewModel::class.java)) {
            return TaskViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

Hier erkennst du das Muster deutlich: Der Aufrufer möchte ein TaskViewModel, aber die Factory weiß, wie es gebaut wird. Sie enthält keine UI-Logik und keine Datenlogik. Sie übersetzt nur eine Anforderung in eine konkrete Instanz.

Eine praktische Entscheidungsregel: Erzeuge Objekte direkt, solange der Konstruktor kurz, stabil und lokal verständlich ist. Verwende eine Factory, sobald die Erzeugung mehrere Schritte, mehrere konkrete Klassen, Varianten oder testrelevante Entscheidungen enthält. Wenn du beim Lesen einer Feature-Datei mehr über Retrofit, Room, Dispatcher oder BuildConfig lernst als über das eigentliche Feature, ist das ein Hinweis auf fehlende Zentralisierung.

Eine typische Stolperfalle ist die überladene Factory. Manche Entwickler verschieben nicht nur Konstruktion, sondern auch Geschäftsregeln in die Factory. Dann entscheidet die Factory plötzlich, welche Aufgaben sichtbar sind, wie Fehler behandelt werden oder wann Daten aktualisiert werden. Das ist zu viel. Eine Factory darf auswählen, welche Implementierung gebaut wird, etwa FakeTaskRepository für Tests oder DefaultTaskRepository für Produktion. Sie sollte aber nicht die Fachlogik des Repositories übernehmen.

Eine zweite Stolperfalle ist versteckter globaler Zustand. Ein object AppFactory mit hart codierten Singletons wirkt bequem, kann Tests aber erschweren. Wenn deine Factory alles selbst aus globalen Quellen holt, ist sie schwer austauschbar. Besser ist oft, die äußeren Ressourcen explizit zu übergeben: Context, API, Datenbank, Dispatcher oder Konfiguration. Dadurch siehst du in Tests sofort, welche Abhängigkeiten ersetzt werden müssen.

Auch Compose verdient Aufmerksamkeit. Composables können häufig erneut ausgeführt werden. Teure Objekte wie Datenbanken, Retrofit-Instanzen oder Repositories solltest du nicht gedankenlos direkt im Composable erzeugen. Entweder kommen sie von außen hinein, werden über eine geeignete Container-Struktur bereitgestellt oder mit passender Lebensdauer gehalten. Eine Factory hilft dir, die Erzeugung aus der UI herauszuziehen. Sie ersetzt aber nicht das Verständnis dafür, wann ein Objekt leben und wann es freigegeben werden soll.

Für Tests ist das Pattern besonders nützlich. Du kannst eine Factory so schneiden, dass Produktionscode echte Implementierungen erhält und Tests kontrollierte Fakes. Beispiel: Deine TaskViewModelFactory bekommt im Test ein FakeTaskRepository. Dann testest du das ViewModel ohne Netzwerk und ohne Datenbank. Das macht Fehler reproduzierbarer und Tests schneller.

In Code-Reviews kannst du gezielt nach drei Fragen suchen. Erstens: Wird hier ein Objekt an einer Stelle erzeugt, die eigentlich Fachlogik oder UI enthalten sollte? Zweitens: Kennt der Aufrufer konkrete Klassen, obwohl ein Interface reichen würde? Drittens: Ist die Konstruktion so komplex, dass ein Test viele irrelevante Details vorbereiten muss? Wenn eine dieser Fragen mit Ja beantwortet wird, kann eine Factory die Struktur verbessern.

Fazit

Das Factory Pattern ist kein großes Framework, sondern eine klare Zuständigkeitsentscheidung: Du bündelst Objekterzeugung und Konfiguration dort, wo sie hingehören, damit Feature-Code lesbarer, testbarer und weniger gekoppelt bleibt. Prüfe dein Verständnis praktisch, indem du in einem bestehenden Android-Projekt eine Stelle suchst, an der UI- oder ViewModel-Code mehrere konkrete Abhängigkeiten selbst baut; ziehe diese Konstruktion in eine kleine Factory, schreibe einen Test mit einer Fake-Implementierung und kontrolliere im Debugger, wann welche Instanz wirklich erzeugt wird.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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