Android architecture: MVVM + UDF
A arquitetura padrão Android 2026
Depois de uma década de experimentação (MVP, MVI, MVVM clássico, Redux-like), a indústria convergiu para MVVM + UDF + Repository + Hilt. Guia oficial do Android corrobora; codebases sérias seguem praticamente a mesma estrutura. Este módulo mostra essa stack canônica.
Camadas
app/
├── data/
│ ├── remote/ (Retrofit services, DTOs)
│ ├── local/ (Room DAOs, entities)
│ └── repository/ (impl que junta remote + local)
├── domain/ (modelos puros, use cases opcionais)
├── ui/
│ ├── feature/users/
│ │ ├── UsersScreen.kt (@Composable)
│ │ ├── UsersViewModel.kt
│ │ └── UsersUiState.kt
│ └── theme/
└── di/ (Hilt modules)UiState + UiEvent
data class UsersUiState(
val loading: Boolean = false,
val users: List<User> = emptyList(),
val error: String? = null,
val searchQuery: String = "",
)
sealed interface UsersIntent {
data object Refresh : UsersIntent
data class Search(val query: String) : UsersIntent
data class Delete(val id: Long) : UsersIntent
}
sealed interface UsersEffect {
data class Toast(val message: String) : UsersEffect
data class Navigate(val userId: Long) : UsersEffect
}Repository
interface UserRepository {
fun observeAll(): Flow<List<User>>
suspend fun refresh()
suspend fun delete(id: Long)
}
class UserRepositoryImpl @Inject constructor(
private val api: UsersApi,
private val dao: UserDao,
@IoDispatcher private val io: CoroutineDispatcher,
) : UserRepository {
override fun observeAll(): Flow<List<User>> =
dao.observeAll().map { entities -> entities.map { it.toDomain() } }
override suspend fun refresh() = withContext(io) {
val fresh = api.users()
dao.upsertAll(fresh.map { it.toEntity() })
}
override suspend fun delete(id: Long) = withContext(io) {
api.delete(id)
dao.delete(id)
}
}ViewModel com Hilt
@HiltViewModel
class UsersViewModel @Inject constructor(
private val repo: UserRepository,
) : ViewModel() {
private val _state = MutableStateFlow(UsersUiState(loading = true))
val state: StateFlow<UsersUiState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<UsersEffect>()
val effects: SharedFlow<UsersEffect> = _effects.asSharedFlow()
init {
viewModelScope.launch {
repo.observeAll().collect { list ->
_state.update { it.copy(users = list, loading = false) }
}
}
onIntent(UsersIntent.Refresh)
}
fun onIntent(intent: UsersIntent) {
when (intent) {
UsersIntent.Refresh -> viewModelScope.launch {
runCatching { repo.refresh() }
.onFailure { t ->
_state.update { it.copy(error = t.message) }
_effects.emit(UsersEffect.Toast("Falha ao atualizar"))
}
}
is UsersIntent.Search -> _state.update { it.copy(searchQuery = intent.query) }
is UsersIntent.Delete -> viewModelScope.launch { repo.delete(intent.id) }
}
}
}DI com Hilt
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Module @InstallIn(SingletonComponent::class)
object AppModule {
@Provides @IoDispatcher
fun provideIo(): CoroutineDispatcher = Dispatchers.IO
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.exemplo.com/")
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
@Provides @Singleton
fun provideUsersApi(r: Retrofit): UsersApi = r.create(UsersApi::class.java)
}
@Module @InstallIn(SingletonComponent::class)
abstract class RepoModule {
@Binds abstract fun bindUserRepo(impl: UserRepositoryImpl): UserRepository
}Screen renderizando state
@Composable
fun UsersScreen(
vm: UsersViewModel = hiltViewModel(),
onNavigateToDetail: (Long) -> Unit,
) {
val ui by vm.state.collectAsStateWithLifecycle()
val ctx = LocalContext.current
LaunchedEffect(Unit) {
vm.effects.collect { effect ->
when (effect) {
is UsersEffect.Toast -> Toast.makeText(ctx, effect.message, Toast.LENGTH_SHORT).show()
is UsersEffect.Navigate -> onNavigateToDetail(effect.userId)
}
}
}
UsersContent(
state = ui,
onIntent = vm::onIntent,
)
}Use cases (opcional)
Em apps grandes, use cases (classe por ação de domínio) ajudam a isolar regra de negócio reutilizável (ex: CalcularFreteUseCase). Em apps pequenos/médios, ViewModel chamando Repository direto já basta — adicionar camada por dogma gera verbosidade sem ganho.
Anti-patterns clássicos
Evite: (1) Context dentro do ViewModel (memory leak), (2) state mutável exposto (var users em vez de StateFlow), (3) LiveData + StateFlow misturados, (4) Retrofit direto na Activity/Composable, (5) delay em produção para simular carregamento.
Próximos passos sugeridos
Discussão
Carregando…