MVVM vs MVI in Android: Welche Architektur in 2026 wählen?

Ausführlicher Vergleich von MVVM und MVI in Android: Vorteile, Einschränkungen, Anwendungsfälle und ein praxisnaher Leitfaden zur Architekturwahl in 2026.

Vergleich der MVVM- und MVI-Architekturen für Android

Die Wahl der richtigen Architektur ist eine entscheidende Entscheidung, die die Wartbarkeit, Testbarkeit und Skalierbarkeit einer Android-Anwendung beeinflusst. Im Jahr 2026 dominieren zwei Muster das Ökosystem: MVVM, der Industriestandard, und MVI, der reaktive Ansatz, der mit Jetpack Compose zunehmend an Bedeutung gewinnt.

Die Einsätze sind hoch

Eine schlechte Architekturwahl ist teuer: technische Schulden, schwer reproduzierbare Fehler und schmerzhafte Refaktorierungen. Das Verständnis der Stärken und Schwächen jedes Ansatzes erspart langfristig erhebliche Probleme.

MVVM verstehen: Der etablierte Standard

MVVM (Model-View-ViewModel) ist Googles empfohlene Architektur seit der Einführung von Jetpack. Sie trennt Verantwortlichkeiten sauber in drei Schichten und macht den Code übersichtlicher und testbarer.

MVVM-Grundprinzipien

Das MVVM-Muster basiert auf einer klaren Trennung: Das Model verwaltet Daten und Geschäftslogik, die View zeigt die Oberfläche an, und das ViewModel verbindet beide, indem es beobachtbare Zustände bereitstellt.

Im Folgenden wird ein Benutzerprofil-Bildschirm mit MVVM implementiert. Dieses erste Beispiel zeigt die grundlegende Struktur mit einem ViewModel, das beobachtbaren Zustand und Methoden für Benutzerinteraktionen bereitstellt.

UserProfileViewModel.ktkotlin
// Classic MVVM ViewModel for a user profile screen
// It exposes observable state and methods for actions
class UserProfileViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Observable state with StateFlow - the View observes these changes
    private val _uiState = MutableStateFlow(UserProfileState())
    val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()

    // Separate loading state - MVVM allows multiple flows
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    // One-shot error messages
    private val _errorMessage = MutableSharedFlow<String>()
    val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()

    // Initial profile loading
    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _isLoading.value = true

            try {
                // Repository call to fetch data
                val user = userRepository.getUser(userId)

                // Update state with new data
                _uiState.update { currentState ->
                    currentState.copy(
                        user = user,
                        isEditing = false
                    )
                }

                // Analytics tracking
                analyticsTracker.trackProfileViewed(userId)

            } catch (e: Exception) {
                // Emit one-shot error message
                _errorMessage.emit("Unable to load profile")
            } finally {
                _isLoading.value = false
            }
        }
    }

    // Enable edit mode
    fun enableEditMode() {
        _uiState.update { it.copy(isEditing = true) }
    }

    // Save profile changes
    fun saveProfile(name: String, bio: String) {
        viewModelScope.launch {
            _isLoading.value = true

            try {
                val updatedUser = userRepository.updateUser(
                    _uiState.value.user?.id ?: return@launch,
                    name = name,
                    bio = bio
                )

                _uiState.update {
                    it.copy(user = updatedUser, isEditing = false)
                }

            } catch (e: Exception) {
                _errorMessage.emit("Failed to save profile")
            } finally {
                _isLoading.value = false
            }
        }
    }
}

// Data class representing the screen state
data class UserProfileState(
    val user: User? = null,
    val isEditing: Boolean = false
)

Dieses ViewModel veranschaulicht den typischen MVVM-Ansatz: mehrere beobachtbare Flows (Hauptzustand, Laden, Fehler) und öffentliche Methoden für jede Benutzeraktion.

Vorteile von MVVM

MVVM hat mehrere Stärken, die seine breite Adoption erklären:

  • Bekanntheit: Die meisten Android-Entwickler kennen dieses Muster
  • Flexibilität: Der Zustand kann beliebig strukturiert werden
  • Ökosystem: Perfekte Integration mit Jetpack (LiveData, StateFlow, Hilt)
  • Einfachheit: Flache Lernkurve für Einsteiger

MVVM eignet sich besonders für gemischte Teams mit Entwicklern unterschiedlicher Erfahrungsstufen. Die konzeptionelle Einfachheit erleichtert das Onboarding.

Einschränkungen von MVVM

MVVM zeigt jedoch seine Grenzen, wenn die Anwendung wächst. Das Hauptproblem ist das verteilte Zustandsmanagement. Das folgende Beispiel veranschaulicht dieses häufige Problem:

ProblematicViewModel.ktkotlin
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {

    // Problem: state scattered across multiple flows
    private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
    private val _selectedAddress = MutableStateFlow<Address?>(null)
    private val _selectedPayment = MutableStateFlow<PaymentMethod?>(null)
    private val _promoCode = MutableStateFlow<String?>(null)
    private val _isLoading = MutableStateFlow(false)
    private val _error = MutableStateFlow<String?>(null)

    // Each modification can create temporary inconsistent states
    fun applyPromoCode(code: String) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null  // Reset error

            try {
                val discount = promoRepository.validate(code)
                _promoCode.value = code
                // Cart state also needs updating...
                // but there's a delay between the two updates
                recalculateCart()
            } catch (e: Exception) {
                _error.value = e.message
                _promoCode.value = null
            } finally {
                _isLoading.value = false
            }
        }
    }

    // Hard to guarantee consistency across all these states
    private fun recalculateCart() {
        // Complex logic depending on multiple states...
    }
}

Dieses Beispiel zeigt, wie der Zustand in MVVM fragmentiert werden kann, was das Nachverfolgen von Übergängen und das Reproduzieren von Fehlern erschwert.

MVI verstehen: Der unidirektionale Ansatz

MVI (Model-View-Intent) verfolgt eine andere Philosophie: unidirektionaler Datenfluss und ein einzelner unveränderlicher Zustand. Dieser von Redux inspirierte Ansatz eliminiert Probleme mit inkonsistentem Zustand.

MVI-Grundprinzipien

In MVI folgt alles einem klaren Zyklus: Der Benutzer sendet einen Intent (Aktion), der Reducer transformiert den aktuellen Zustand in einen neuen, und die View zeigt diesen einzelnen Zustand an. Das ist vorhersehbar, testbar und debuggbar.

Im Folgenden wird derselbe Benutzerprofil-Bildschirm mit MVI implementiert. Dabei fällt auf, wie der Zustand zentralisiert und die Aktionen explizit typisiert sind.

UserProfileMviViewModel.ktkotlin
// MVI ViewModel for the same user profile screen
// Note the structure: Intent → Reducer → Single State
class UserProfileMviViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Single, immutable state - the absolute source of truth
    private val _state = MutableStateFlow(UserProfileState())
    val state: StateFlow<UserProfileState> = _state.asStateFlow()

    // Channel for side effects (navigation, snackbar)
    private val _sideEffect = Channel<UserProfileSideEffect>()
    val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()

    // Single entry point for all user actions
    fun onIntent(intent: UserProfileIntent) {
        when (intent) {
            is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
            is UserProfileIntent.EnableEditMode -> enableEditMode()
            is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
            is UserProfileIntent.CancelEdit -> cancelEdit()
        }
    }

    private fun loadProfile(userId: String) {
        viewModelScope.launch {
            // Transition to loading state
            _state.update { it.copy(isLoading = true, error = null) }

            try {
                val user = userRepository.getUser(userId)

                // Single atomic state update
                _state.update {
                    it.copy(
                        user = user,
                        isLoading = false,
                        error = null
                    )
                }

                analyticsTracker.trackProfileViewed(userId)

            } catch (e: Exception) {
                // Error state is part of the main state
                _state.update {
                    it.copy(
                        isLoading = false,
                        error = "Unable to load profile"
                    )
                }
            }
        }
    }

    private fun enableEditMode() {
        // Simple, predictable update
        _state.update { it.copy(isEditing = true) }
    }

    private fun saveProfile(name: String, bio: String) {
        viewModelScope.launch {
            val currentUser = _state.value.user ?: return@launch

            _state.update { it.copy(isLoading = true) }

            try {
                val updatedUser = userRepository.updateUser(
                    currentUser.id,
                    name = name,
                    bio = bio
                )

                _state.update {
                    it.copy(
                        user = updatedUser,
                        isEditing = false,
                        isLoading = false
                    )
                }

                // Side effect to notify the user
                _sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))

            } catch (e: Exception) {
                _state.update {
                    it.copy(isLoading = false, error = "Failed to save profile")
                }
            }
        }
    }

    private fun cancelEdit() {
        _state.update { it.copy(isEditing = false) }
    }
}

// All possible actions, explicitly typed
sealed class UserProfileIntent {
    data class LoadProfile(val userId: String) : UserProfileIntent()
    object EnableEditMode : UserProfileIntent()
    data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
    object CancelEdit : UserProfileIntent()
}

// Single, complete screen state
data class UserProfileState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val isEditing: Boolean = false,
    val error: String? = null
)

// One-shot side effects
sealed class UserProfileSideEffect {
    data class ShowSuccess(val message: String) : UserProfileSideEffect()
    data class NavigateTo(val destination: String) : UserProfileSideEffect()
}

Der Unterschied ist klar: ein einzelner Zustandsflow, explizite Aktionen und eine saubere Trennung zwischen persistentem Zustand und einmaligen Effekten.

Einfacheres Debugging

Mit MVI lässt sich jeder Intent und jeder Zustandsübergang protokollieren. Einen Fehler zu reproduzieren wird trivial: Einfach die Abfolge der Intents wiederholen.

MVI mit Jetpack Compose

MVI glänzt besonders mit Jetpack Compose, da beide dieselbe Philosophie teilen: unveränderlicher Zustand und deklarative UI. So wird das ViewModel mit einem Compose-Screen verbunden:

UserProfileScreen.ktkotlin
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
    viewModel: UserProfileMviViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    // Collect the single state
    val state by viewModel.state.collectAsStateWithLifecycle()

    // Handle side effects
    LaunchedEffect(Unit) {
        viewModel.sideEffect.collect { effect ->
            when (effect) {
                is UserProfileSideEffect.ShowSuccess -> {
                    // Show snackbar
                }
                is UserProfileSideEffect.NavigateTo -> {
                    // Navigate
                }
            }
        }
    }

    // Purely declarative UI based on state
    UserProfileContent(
        state = state,
        onIntent = viewModel::onIntent
    )
}

@Composable
private fun UserProfileContent(
    state: UserProfileState,
    onIntent: (UserProfileIntent) -> Unit
) {
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {

        // Conditional rendering based on the single state
        when {
            state.isLoading -> {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }
            state.error != null -> {
                ErrorMessage(
                    message = state.error,
                    onRetry = {
                        state.user?.id?.let {
                            onIntent(UserProfileIntent.LoadProfile(it))
                        }
                    }
                )
            }
            state.user != null -> {
                ProfileCard(
                    user = state.user,
                    isEditing = state.isEditing,
                    onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
                    onSaveClick = { name, bio ->
                        onIntent(UserProfileIntent.SaveProfile(name, bio))
                    },
                    onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
                )
            }
        }
    }
}

Die UI wird zur reinen Funktion des Zustands: vorhersehbar, testbar und ohne versteckte Seiteneffekte.

Detaillierter Vergleich

Nachdem beide Muster in der Praxis gesehen wurden, werden sie anhand der Kriterien verglichen, die in der Produktion wirklich wichtig sind.

Zustandsverwaltung

Der grundlegende Unterschied liegt in der Zustandsverwaltung. Diese Unterscheidung beeinflusst direkt die langfristige Wartbarkeit.

StateComparison.ktkotlin
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
    // Multiple sources of truth - manual synchronization needed
    private val _users = MutableStateFlow<List<User>>(emptyList())
    private val _selectedUser = MutableStateFlow<User?>(null)
    private val _isLoading = MutableStateFlow(false)
    private val _searchQuery = MutableStateFlow("")

    // What happens if _selectedUser points to a user
    // that's no longer in _users after a refresh?
    // → Inconsistent state that's hard to detect
}

// MVI: consistent state by construction
class MviViewModel : ViewModel() {
    // Single source of truth - inconsistencies are impossible
    private val _state = MutableStateFlow(UsersState())

    data class UsersState(
        val users: List<User> = emptyList(),
        val selectedUser: User? = null,  // Always consistent with users
        val isLoading: Boolean = false,
        val searchQuery: String = ""
    )

    // Each update automatically maintains invariants
    private fun selectUser(userId: String) {
        _state.update { currentState ->
            currentState.copy(
                selectedUser = currentState.users.find { it.id == userId }
            )
        }
    }
}
Geisterfehler

In MVVM manifestieren sich inkonsistente Zustände oft als intermittierende Fehler, die schwer zu reproduzieren sind. In MVI ist ein ungültiger Zustand deterministisch ungültig.

Testbarkeit der Architektur

Beide Architekturen sind testbar, aber MVI bietet durch seine Vorhersehbarkeit einen wesentlichen Vorteil.

TestComparison.ktkotlin
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
    val viewModel = MvvmViewModel(fakeRepository)

    // Observe multiple flows simultaneously
    val users = mutableListOf<List<User>>()
    val loadingStates = mutableListOf<Boolean>()

    val job1 = launch { viewModel.users.toList(users) }
    val job2 = launch { viewModel.isLoading.toList(loadingStates) }

    viewModel.loadUsers()
    advanceUntilIdle()

    // Assertions on different flows
    assertThat(users.last()).isEqualTo(expectedUsers)
    assertThat(loadingStates).containsExactly(false, true, false)

    job1.cancel()
    job2.cancel()
}

// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
    val viewModel = MviViewModel(fakeRepository)

    // Collect all states in order
    val states = mutableListOf<UsersState>()
    val job = launch { viewModel.state.toList(states) }

    // Send the intent
    viewModel.onIntent(UsersIntent.LoadUsers)
    advanceUntilIdle()

    // Verify the exact state sequence
    assertThat(states).containsExactly(
        UsersState(),                                    // Initial
        UsersState(isLoading = true),                   // Loading
        UsersState(users = expectedUsers, isLoading = false)  // Success
    )

    job.cancel()
}

MVI ermöglicht es, die genaue Abfolge der Zustandsübergänge zu testen, was besonders nützlich für komplexe Screens mit vielen Interaktionen ist.

Komplexität und Boilerplate

Die Kompromisse sollten offen benannt werden. MVI erfordert mehr Boilerplate-Code und ein tieferes Verständnis der Konzepte.

BoilerplateComparison.ktkotlin
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
    private val _name = MutableStateFlow("")
    val name: StateFlow<String> = _name.asStateFlow()

    fun updateName(newName: String) {
        _name.value = newName
    }
}
// Total: ~10 lines

// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
    private val _state = MutableStateFlow(SimpleState())
    val state: StateFlow<SimpleState> = _state.asStateFlow()

    fun onIntent(intent: SimpleIntent) {
        when (intent) {
            is SimpleIntent.UpdateName -> {
                _state.update { it.copy(name = intent.name) }
            }
        }
    }
}

data class SimpleState(val name: String = "")

sealed class SimpleIntent {
    data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~20 lines

Für einen einfachen Screen kann MVI übertrieben erscheinen. Aber diese Struktur zahlt sich aus, wenn der Screen an Komplexität gewinnt.

Bereit für deine Android-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Wann sollte MVVM gewählt werden?

MVVM bleibt in mehreren Situationen die pragmatische Wahl:

Bestehende Projekte

Wenn die Anwendung bereits MVVM verwendet, ist die Migration zu MVI mit erheblichem Aufwand verbunden. Die Verbesserung der bestehenden MVVM-Struktur ist oft die sinnvollere Entscheidung.

Junior- oder gemischte Teams

MVVM ist zugänglicher. Ein Team mit Einsteigern wird mit MVVM schneller produktiv als mit MVI.

Einfache Screens

Für Screens mit wenigen Zuständen und Interaktionen fügt MVI Komplexität ohne proportionalen Nutzen hinzu.

SettingsViewModel.ktkotlin
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
    private val preferencesRepository: PreferencesRepository
) : ViewModel() {

    val darkMode = preferencesRepository.darkModeFlow
        .stateIn(viewModelScope, SharingStarted.Lazily, false)

    val notificationsEnabled = preferencesRepository.notificationsFlow
        .stateIn(viewModelScope, SharingStarted.Lazily, true)

    fun toggleDarkMode() {
        viewModelScope.launch {
            preferencesRepository.setDarkMode(!darkMode.value)
        }
    }

    fun toggleNotifications() {
        viewModelScope.launch {
            preferencesRepository.setNotifications(!notificationsEnabled.value)
        }
    }
}

Wann sollte MVI gewählt werden?

MVI zeigt seinen Wert in bestimmten Kontexten:

Anwendungen mit komplexem Zustand

Wenn ein Screen viele voneinander abhängige Zustände hat, gewährleistet MVI die Konsistenz.

CheckoutState.ktkotlin
// Checkout screen with complex state: MVI excels
data class CheckoutState(
    val cartItems: List<CartItem> = emptyList(),
    val selectedAddress: Address? = null,
    val selectedPayment: PaymentMethod? = null,
    val promoCode: PromoCode? = null,
    val deliveryOptions: List<DeliveryOption> = emptyList(),
    val selectedDelivery: DeliveryOption? = null,
    val subtotal: Money = Money.ZERO,
    val discount: Money = Money.ZERO,
    val deliveryFee: Money = Money.ZERO,
    val total: Money = Money.ZERO,
    val isLoading: Boolean = false,
    val error: CheckoutError? = null,
    val step: CheckoutStep = CheckoutStep.CART
) {
    // Verifiable invariants
    init {
        require(total == subtotal - discount + deliveryFee) {
            "Total inconsistent with components"
        }
    }
}

sealed class CheckoutIntent {
    data class AddItem(val item: CartItem) : CheckoutIntent()
    data class RemoveItem(val itemId: String) : CheckoutIntent()
    data class SelectAddress(val address: Address) : CheckoutIntent()
    data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
    data class ApplyPromo(val code: String) : CheckoutIntent()
    object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    object ProceedToPayment : CheckoutIntent()
    object ConfirmOrder : CheckoutIntent()
}

Echtzeit-Anwendungen

Für Apps mit WebSockets, Push-Benachrichtigungen oder Echtzeit-Synchronisierung verwaltet MVI mehrere Datenströme elegant.

Strenge Debugging-Anforderungen

In regulierten Bereichen (Fintech, Gesundheit) ist die Fähigkeit, eine genaue Ereignissequenz zu reproduzieren, unbezahlbar.

MVI macht es einfach, "Time-Travel-Debugging" zu implementieren: alle Zustände aufzeichnen und die Benutzersitzung wiedergeben.

Hybrider Ansatz: Das Beste aus beiden Welten

In der Praxis adoptieren viele Teams einen hybriden Ansatz: MVI für komplexe Screens, vereinfachtes MVVM für einfache Screens. Hier ist ein empfohlenes Muster:

MviViewModel.ktkotlin
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {

    private val _state = MutableStateFlow(initialState)
    val state: StateFlow<S> = _state.asStateFlow()

    protected val currentState: S get() = _state.value

    // Single entry point for intents
    abstract fun onIntent(intent: I)

    // Helper to update state
    protected fun updateState(reducer: S.() -> S) {
        _state.update { it.reducer() }
    }
}

// Concrete implementation stays simple
class ProfileViewModel(
    private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {

    override fun onIntent(intent: ProfileIntent) {
        when (intent) {
            is ProfileIntent.Load -> load(intent.userId)
            is ProfileIntent.Refresh -> refresh()
            is ProfileIntent.ToggleFavorite -> toggleFavorite()
        }
    }

    private fun load(userId: String) {
        viewModelScope.launch {
            updateState { copy(isLoading = true) }

            val user = userRepository.getUser(userId)

            updateState {
                copy(user = user, isLoading = false)
            }
        }
    }

    private fun refresh() = load(currentState.user?.id ?: return)

    private fun toggleFavorite() {
        updateState {
            copy(user = user?.copy(isFavorite = !user.isFavorite))
        }
    }
}

Dieser Ansatz bietet die Vorteile von MVI (einzelner Zustand, typisierte Intents) ohne übermäßigen Boilerplate.

Empfehlungen für 2026

Hier sind die Empfehlungen für die Wahl zwischen diesen beiden Architekturen je nach Kontext:

Für neue Projekte mit Compose

MVI von Anfang an einsetzen. Compose und MVI teilen dieselbe Philosophie, und die anfängliche Investition amortisiert sich schnell.

Für bestehende View-basierte Projekte

Bei MVVM bleiben, aber schrittweise MVI-Best-Practices übernehmen: einzelner Zustand im ViewModel, typisierte Aktionen mit Sealed Classes.

Für große Teams

Auf einen Ansatz standardisieren und ihn dokumentieren. Konsistenz im Code ist wichtiger als die Wahl des Musters selbst.

Das eigentliche Kriterium

Das beste Muster ist das, das das Team versteht und korrekt anwendet. Ein gut implementiertes MVVM schlägt ein schlecht verstandenes MVI.

Fazit

MVVM und MVI sind beide valide Ansätze für die Architektur von Android-Anwendungen. MVVM bietet Einfachheit und Vertrautheit, während MVI Vorhersehbarkeit und einfacheres Debugging bringt.

Entscheidungs-Checkliste

  • MVVM wählen, wenn: Junior-Team, einfaches Projekt, kostspielige Migration
  • MVI wählen, wenn: natives Compose, komplexer Zustand, kritisches Debugging
  • Hybrider Ansatz empfohlen: leichtgewichtiges MVI mit einzelnem Zustand, ohne Over-Engineering
  • Höchste Priorität: Konsistenz im gesamten Code

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Unabhängig von der Wahl liegt der Schlüssel darin, die Stärken und Schwächen jedes Ansatzes zu verstehen, um eine fundierte Entscheidung zu treffen. Der beste Code ist jener, den das Team langfristig gelassen warten kann.

Tags

#android
#mvvm
#mvi
#architecture
#jetpack compose

Teilen

Verwandte Artikel