Настройка архітектури MVVM для Android-застосунку
MVVM — де-факто стандарт для Android з моменту, коли Google випустив Architecture Components у 2017 році. ViewModel + LiveData/StateFlow + Repository — ця комбінація описана в офіційних гайдлайнах, підтримується Jetpack та розуміється будь-яким Android-розробником. Проблема не в тому, що MVVM складен — а в тому, що його легко реалізувати неправильно.
Типові помилки, які ломають архітектуру
ViewModel з контекстом. Якщо ViewModel зберігає Context або ссилку на Activity/Fragment — це утечка пам'яті та порушення смислу паттерну. ViewModel переживає пересоздання Activity при повороті екрана. Для роботи з ресурсами використовуємо AndroidViewModel з Application-контекстом тільки коли іншого виходу немає, або виносимо рядки/ресурси в окремий слой.
LiveData в Repository. Repository повертає LiveData<List<User>> — та тепер Repository пов'язаний з Android-фреймворком. Правильно: Repository працює з Flow<List<User>> (coroutines), ViewModel конвертує через stateIn або .asLiveData().
Бізнес-логіка у ViewModel. ViewModel повинна трансформувати дані для UI, не реалізувати бізнес-правила. Складна логіка — у UseCase-класах між ViewModel та Repository.
Як виглядає правильний MVVM на Kotlin
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
getUserProfile(userId)
.onSuccess { _uiState.value = ProfileUiState.Success(it) }
.onFailure { _uiState.value = ProfileUiState.Error(it.message) }
}
}
}
sealed class ProfileUiState {
object Loading : ProfileUiState()
data class Success(val profile: UserProfile) : ProfileUiState()
data class Error(val message: String?) : ProfileUiState()
}
У Fragment або Composable підписуємось на uiState через collectAsStateWithLifecycle() — це безпечніше, ніж collect, тому що автоматично зупиняє збір при переході в background.
Repository та джерела даних
Repository — єдина точка входу для ViewModel у дані. Реалізує інтерфейс-протокол. Всередину вирішує: брати з Room, з Retrofit або з кешу:
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override fun getProfile(id: String): Flow<UserProfile> = flow {
val cached = dao.getUser(id)
if (cached != null) emit(cached.toDomain())
val remote = api.fetchUser(id)
dao.insert(remote.toEntity())
emit(remote.toDomain())
}
}
Hilt для DI
Без Hilt у Android MVVM доводиться вручну створювати ViewModelFactory. Hilt (@HiltViewModel + @Inject) позбавляє від цього: Dagger-граф генерується на етапі компіляції, помилки конфігурації видні одразу, а не у рантайме.
Що настраюємо
Структура пакетів: data (api, db, repository impl), domain (models, repository interfaces, use cases), presentation (viewmodels, ui). Підключення Hilt. Настройка базового Repository з Room + Retrofit. Образцовий ViewModel з StateFlow та sealed class для UI-станів. Тести через kotlinx-coroutines-test та turbine.
Терміни
Настройка MVVM-структури з нуля: 2–4 дня. Рефакторинг існуючого Activity-based проекту: 1–3 тижні. Вартість розраховується індивідуально.







