MVVM vs MVI: Quale Architettura Scegliere nel 2026?

Confronto approfondito tra MVVM e MVI su Android: pro, contro, casi d'uso e una guida pratica per scegliere l'architettura giusta. Aggiornato con Kotlin 2.4 explicit backing fields.

Confronto tra le architetture MVVM e MVI per Android

Scegliere l'architettura giusta è una decisione cruciale che influenza la manutenibilità, la testabilità e la scalabilità di un'applicazione Android. Nel 2026, due pattern dominano l'ecosistema: MVVM, lo standard del settore, e MVI, l'approccio reattivo che si è affermato come complemento naturale per Jetpack Compose.

La posta in gioco è alta

Una scelta architettonica errata è costosa: debito tecnico, bug difficili da riprodurre e refactoring dolorosi. Comprendere i punti di forza e di debolezza di ciascun approccio risparmia notevoli problemi nel lungo periodo.

Comprendere MVVM: Lo Standard Consolidato

MVVM (Model-View-ViewModel) è l'architettura consigliata da Google fin dall'introduzione di Jetpack. Separa le responsabilità in tre livelli distinti, rendendo il codice più organizzato e testabile.

Principi Fondamentali di MVVM

Il pattern MVVM si basa su una separazione netta: il Model gestisce dati e logica di business, la View visualizza l'interfaccia, e il ViewModel fa da ponte esponendo stati osservabili.

Questo primo esempio mostra la struttura base con un ViewModel che espone stato osservabile e metodi per le interazioni utente. Da notare come gli explicit backing fields di Kotlin 2.4 eliminino il tradizionale pattern boilerplate _uiState / uiState.

UserProfileViewModel.ktkotlin
// MVVM ViewModel with Kotlin 2.4 explicit backing fields
// The backing field syntax eliminates the _state / state dance
class UserProfileViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Kotlin 2.4: explicit backing field - no more _uiState / uiState pair
    val uiState: StateFlow<UserProfileState>
        field = MutableStateFlow(UserProfileState())

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

    // One-shot error messages via SharedFlow
    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
)

Questo ViewModel illustra l'approccio MVVM tipico: più flow osservabili (stato principale, caricamento, errori) e metodi pubblici per ogni azione utente. La sintassi degli explicit backing fields di Kotlin 2.4 rende il codice più pulito dichiarando il tipo della proprietà come StateFlow mentre il backing field è MutableStateFlow.

Vantaggi di MVVM

MVVM presenta diversi punti di forza che ne spiegano l'adozione massiccia:

  • Familiarità: La maggior parte degli sviluppatori Android conosce questo pattern
  • Flessibilità: Lo stato può essere strutturato secondo le esigenze del caso d'uso
  • Ecosistema: Integrazione perfetta con Jetpack (LiveData, StateFlow, Hilt)
  • Semplicità: Curva di apprendimento graduale per i principianti

MVVM è particolarmente adatto per team misti con sviluppatori di diversi livelli. La sua semplicità concettuale facilita l'onboarding.

Limiti di MVVM

Tuttavia, MVVM mostra i suoi limiti man mano che l'applicazione cresce. Il problema principale è la gestione distribuita dello stato. Questo esempio illustra il problema comune dello stato frammentato:

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
    val cart: StateFlow<List<CartItem>>
        field = MutableStateFlow(emptyList())
    
    val selectedAddress: StateFlow<Address?>
        field = MutableStateFlow(null)
    
    val selectedPayment: StateFlow<PaymentMethod?>
        field = MutableStateFlow(null)
    
    val promoCode: StateFlow<String?>
        field = MutableStateFlow(null)
    
    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)
    
    val error: StateFlow<String?>
        field = MutableStateFlow(null)

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

            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...
    }
}

Questo esempio mostra come lo stato possa frammentarsi in MVVM, rendendo difficile tracciare le transizioni e riprodurre i bug.

Comprendere MVI: L'Approccio Unidirezionale

MVI (Model-View-Intent) adotta una filosofia diversa: flusso di dati unidirezionale e un singolo stato immutabile. Questo approccio, ispirato a Redux, elimina i problemi di stato inconsistente.

Principi Fondamentali di MVI

In MVI, tutto segue un ciclo chiaro: l'utente emette un Intent (azione), il Reducer trasforma lo stato corrente in uno nuovo, e la View visualizza questo singolo stato. È prevedibile, testabile e debuggabile.

Questa implementazione della stessa schermata del profilo utente dimostra come lo stato sia centralizzato e le azioni siano tipizzate esplicitamente:

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
    val state: StateFlow<UserProfileState>
        field = MutableStateFlow(UserProfileState())

    // 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()
    data object EnableEditMode : UserProfileIntent()
    data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
    data 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()
}

La differenza è evidente: un singolo flow di stato, azioni esplicite e una separazione netta tra stato persistente ed effetti monouso.

Debug più Semplice

Con MVI è possibile registrare ogni Intent e ogni transizione di stato. Riprodurre un bug diventa banale: basta riprodurre la sequenza di Intent.

MVI con Jetpack Compose

MVI brilla particolarmente con Jetpack Compose, poiché entrambi condividono la stessa filosofia: stato immutabile e UI dichiarativa. Ecco come connettere il ViewModel a una schermata Compose:

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) }
                )
            }
        }
    }
}

L'interfaccia diventa una funzione pura dello stato: prevedibile, testabile e senza effetti collaterali nascosti.

Confronto Dettagliato

Ora che entrambi i pattern sono chiari, segue un confronto sui criteri che contano davvero in produzione.

Gestione dello Stato

La differenza fondamentale risiede nella gestione dello stato. Questa distinzione impatta direttamente la manutenibilità a lungo termine.

StateComparison.ktkotlin
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
    // Multiple sources of truth - manual synchronization needed
    val users: StateFlow<List<User>>
        field = MutableStateFlow(emptyList())
    
    val selectedUser: StateFlow<User?>
        field = MutableStateFlow(null)
    
    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)
    
    val searchQuery: StateFlow<String>
        field = 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
    val state: StateFlow<UsersState>
        field = 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 }
            )
        }
    }
}
Bug Fantasma

In MVVM, gli stati inconsistenti si manifestano spesso come bug intermittenti difficili da riprodurre. In MVI, se lo stato è invalido, lo è in modo deterministico.

Testabilità dell'Architettura

Entrambe le architetture sono testabili, ma MVI offre un vantaggio significativo grazie alla sua prevedibilità.

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 consente di verificare la sequenza esatta delle transizioni di stato, particolarmente utile per schermate complesse con molte interazioni.

Complessità e Boilerplate

Da Kotlin 2.4, il divario di boilerplate tra MVVM e MVI si è ridotto significativamente. La funzionalità degli explicit backing fields elimina il pattern _state / state che prima aggiungeva righe a ogni ViewModel.

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

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

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

    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: ~18 lines

Per una schermata semplice, MVI può sembrare eccessivo. Ma questa struttura ripaga man mano che la schermata cresce in complessità.

Pronto a superare i tuoi colloqui su Android?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Quando Scegliere MVVM?

MVVM rimane la scelta pragmatica in diverse situazioni:

Progetti Esistenti

Se l'applicazione usa già MVVM, migrare a MVI rappresenta un effort considerevole. Migliorare la struttura MVVM esistente è spesso la decisione più sensata.

Team Junior o Misti

MVVM è più accessibile. Un team con sviluppatori principianti sarà produttivo più rapidamente con MVVM che con MVI.

Schermate Semplici

Per schermate con pochi stati e interazioni, MVI aggiunge complessità senza beneficio proporzionale.

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)
        }
    }
}

Quando Scegliere MVI?

MVI dimostra il suo valore in contesti specifici:

Applicazioni con Stato Complesso

Quando una schermata ha molti stati interdipendenti, MVI garantisce la coerenza.

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()
    data object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    data object ProceedToPayment : CheckoutIntent()
    data object ConfirmOrder : CheckoutIntent()
}

Applicazioni in Tempo Reale

Per app con WebSocket, notifiche push o sincronizzazione in tempo reale, MVI gestisce elegantemente più flussi di dati.

Requisiti Rigidi di Debugging

In settori regolamentati (fintech, sanità), la capacità di riprodurre esattamente una sequenza di eventi è inestimabile.

MVI facilita l'implementazione del "time-travel debugging": registrare tutti gli stati e riprodurre la sessione utente.

Alternativa Emergente: Circuit

Circuit, sviluppato da Slack, offre un'implementazione MVI nativa per Compose che vale la pena considerare per nuovi progetti. Combina presenter con Molecule sotto il cofano, eliminando gran parte del boilerplate MVI mantenendo il flusso di dati unidirezionale.

Circuit è particolarmente interessante per progetti Kotlin Multiplatform, poiché supporta la condivisione della logica di presentazione tra piattaforme. Tuttavia, l'adozione al di fuori di Slack rimane limitata rispetto all'approccio standard con Jetpack ViewModel.

Approccio Ibrido: Il Meglio dei Due Mondi

Nella pratica, molti team adottano un approccio ibrido: MVI per schermate complesse, MVVM semplificato per schermate semplici. Ecco un pattern consigliato:

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

    val state: StateFlow<S>
        field = MutableStateFlow(initialState)

    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))
        }
    }
}

Questo approccio offre i vantaggi di MVI (stato unico, intent tipizzati) senza boilerplate eccessivo.

Raccomandazioni

Ecco le raccomandazioni per scegliere tra le due architetture in base al contesto:

Per Nuovi Progetti con Compose

Adottare MVI dall'inizio. Compose e MVI condividono la stessa filosofia, e l'investimento iniziale si ripaga rapidamente. Con gli explicit backing fields di Kotlin 2.4, il divario di boilerplate si è ridotto.

Per Progetti Esistenti Basati su View

Mantenere MVVM, ma adottare gradualmente le best practice di MVI: stato unico nel ViewModel, azioni tipizzate con sealed class.

Per Team Grandi

Standardizzarsi su un unico approccio e documentarlo. La coerenza nel codice è più importante della scelta del pattern stesso.

Il Criterio Reale

Il miglior pattern è quello che il team comprende e applica correttamente. Un MVVM ben implementato supera un MVI mal compreso.

Fonti

Conclusione

MVVM e MVI sono entrambi approcci validi per architettare applicazioni Android. MVVM offre semplicità e familiarità, mentre MVI porta prevedibilità e debugging più agevole.

Checklist per la Decisione

  • Scegliere MVVM se: team junior, progetto semplice, migrazione costosa
  • Scegliere MVI se: Compose nativo, stato complesso, debugging critico
  • Approccio ibrido consigliato: MVI leggero con stato unico, senza over-engineering
  • Priorità massima: coerenza in tutto il codice

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Indipendentemente dalla scelta, la chiave è comprendere i punti di forza e di debolezza di ciascun approccio per prendere una decisione informata. Il codice migliore è quello che il team riesce a mantenere serenamente nel lungo periodo.

Sfida del giorno

Sapresti trovare il bug in Android?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 19 agosto 2026

Tag

#android
#mvvm
#mvi
#architecture
#jetpack compose

Condividi

Articoli correlati