MVVM (Model-View-ViewModel) — архитектурални шаблон у коме ViewModel замењује Presenter и користи реактивне механизме за комуникацију са View: ObservableObject у SwiftUI, LiveData/StateFlow у Android-у. ViewModel нема референцу на View — подаци се преносе кроз претплату, што елиминише потребу за интерфејсима ViewContract и чини тестирање још једноставнијим. Apple препоручује MVVM са SwiftUI од 2019. године, Google — MVVM са Jetpack-ом као званичну архитектуру Android-а. Више у Android Architecture Guide-у.
Главно
MVVM (Model-View-ViewModel) — архитектурални шаблон који је описао John Gossman 2005. године за Windows Presentation Foundation (WPF) од Microsoft-а. ViewModel — централна компонента која садржи стање екрана и пословну логику, али нема референцу на View. Подаци се преносе кроз механизме реактивног повезивања: View се претплаћује на промене ViewModel-а и аутоматски се прецртава када се подаци промене.
Кључна разлика MVVM-а од MVP-а — одсуство ViewContract-а. У MVP-у, Presenter позива методе view.showUser(data), односно Presenter активно „гура“ податке у View. У MVVM-у, View сама „вуче“ податке из ViewModel-а кроз претплату: ViewModel не зна да ли има претплатника. Ово елиминише проблем одвојеног View-а — ако је Activity уништено при ротацији, ViewModel наставља рад, а ново Activity се једноставно претплаћује на актуелне податке. У IT Sectr-у користимо MVVM у свим новим пројектима од 2020. године — код је постао предвидљивији, тестови стабилнији.
| Компонента | Одговорност | Платформа |
|---|---|---|
| Model | Подаци, пословна логика, репозиторијуми | Android/iOS |
| View | Приказ, претплата на ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Стање екрана, логика, навигација | ViewModel (Jetpack), ObservableObject |
Реактивно повезивање — темељ MVVM-а. У Android-у, LiveData (део Jetpack-а) је посматрано складиште података. Activity се претплаћује кроз observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. При промени user-а, сви претплатници аутоматски добијају нову вредност. У iOS-у, SwiftUI користи @Published својства у ViewModel-у — промене аутоматски прецртавају View. Ово елиминише ручне позиве showUser/hideLoading, који су потребни у MVP-у.
ViewModel из Jetpack-а — званична компонента од Google-а за имплементацију MVVM-а. ViewModel преживљава ротацију екрана: при промени конфигурације, Activity се уништава и поново креира, а ViewModel остаје у меморији. Нова инстанца Activity-ја добија исти ViewModel кроз ViewModelProvider. ViewModel нема референце на Activity, Context или View — чист је и тестира се unit-тестовима без Robolectric-а.
// ViewModel са StateFlow-ом — савремена имплементација MVVM-а
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View (Activity) се претплаћује на state
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* показать загрузку */
is UserState.Success -> /* отобразить данные */
is UserState.Error -> /* показать ошибку */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — прва реактивна компонента Jetpack-а, оптимизована за животни циклус Activity-ја: аутоматска одјава при onStop. StateFlow (2021) — имплементација Kotlin Flow-а, невезана за животни циклус, али захтева ручну одјаву кроз lifecycleScope. StateFlow подржава coroutines, concat, map и друге Flow операторе, којих нема у LiveData-и. У IT Sectr-у користимо StateFlow за све нове ViewModel-е — краћи је, моћнији и боље се интегрише са корутинама.
DataBinding и ViewBinding — DataBinding повезује ViewModel са XML-ом кроз @{viewModel.user.name} директно у изгледу, елиминишући код у Activity-ју. ViewBinding генерише типно безбедну класу за приступ View-у. Google препоручује ViewBinding за једноставне пројекте и DataBinding за пројекте са сложеним повезивањем података. У Jetpack Compose-у, DataBinding није потребан — @Composable функције се аутоматски прецртавају при промени State-а.
MVVM у iOS-у се имплементира кроз ObservableObject из Combine-а. ViewModel — класа која наслеђује ObservableObject, са @Published пољима. SwiftUI View се претплаћује на ViewModel кроз @ObservedObject или @StateObject. При промени @Published својства, SwiftUI аутоматски прецртава View које зависи од тог својства. Apple је представио SwiftUI 2019. године на WWDC-у заједно са Combine-ом — од тог тренутка MVVM је постао званично препоручени шаблон за iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject са @Published пољима
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View — претплаћује се на ViewModel
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject vs @ObservedObject — @StateObject креира ViewModel и управља његовим животним циклусом (једном током живота View-а). @ObservedObject — ViewModel се креира споља и прослеђује се View-у. WWDC 2022 препоручује @StateObject за креирање и @ObservedObject за прослеђивање ViewModel-а између Views-а. У iOS 17 (2023) појавио се @Observable — макро који аутоматизује претплату и елиминише @Published анотације. @Observable — еволуција Combine-а, која приближава iOS развој реактивности Kotlin Flow-а.
UIKit + MVVM — за пројекте на UIKit-у (без SwiftUI-ја), MVVM се имплементира кроз Combine и @Published са претплатом у UIViewController-у кроз sink(). ViewModel је исти, View — UIViewController са претплатама на @Published. Combine је доступан од iOS 13 (2019) и уграђен је у систем — не захтева додатне зависности. Према Apple Developer Survey (2025), 45% iOS пројеката користи Combine чак и са UIKit-ом, 35% користи SwiftUI + Combine, 20% — RxSwift (legacy).
MVVM побеђује MVP у три кључна аспекта: одсуство интерфејса ViewContract, аутоматско управљање претплатама и преживљавање ротације екрана. У MVP-у, сваки екран захтева интерфејс ViewContract + класу Presenter + претплату/одјаву у onStart/onStop. У MVVM-у се креира само ViewModel — претплата у Activity-ју се врши кроз observe() без ручног detach-а.
| Критеријум | MVP | MVVM |
|---|---|---|
| Интерфејси ViewContract | 1 по екрану | Нису потребни |
| Управљање претплатама | Ручно attach/detach | Аутоматско (lifecycle-aware) |
| Ротација екрана | Retain-фрагмент | ViewModel преживљава |
| Тестирање | Mock ViewContract | Чиста класа без зависности |
| Реактивност | Повратни позиви у Presenter-у | LiveData/StateFlow/Combine |
Мане MVVM-а — сложеност отклањања грешака у реактивним ланцима и ризик од цурења меморије при неправилној претплати. LiveData решава проблем безбедности животног циклуса, StateFlow захтева lifecycleScope, Combine — sink са AnyCancellable. У MVP-у су сви позиви експлицитни (view.showUser), у MVVM-у подаци долазе кроз реактивни ток — праћење захтева debug-прекидне тачке у subscribe затварању. У великим ViewModel-има са више StateFlow-ева, може се пропустити ажурирање UI-ја ако View није претплаћен на одређени Flow.
Када је MVP и даље бољи — у пројектима са минималном верзијом Android-а испод API 21 (Android 5), где Jetpack ViewModel није доступан без AndroidX-а, и у пројектима на чистом UIKit-у без Combine-а (iOS 12 и ниже). За legacy пројекте, где је цела кодна база већ на MVP-у, потпуни прелазак на MVVM није увек оправдан — јефтиније је одржавати MVP са постепеним извлачењем логике у сервисе, него преписивати 100 екрана за 3 месеца.
ViewModel се тестира unit-тестовима без платформских зависности — ово је главни аргумент у корист MVVM-а. На Android-у, ViewModel не садржи Activity, Context или View — све зависности (Repository, UseCase) се прослеђују кроз конструктор и замењују mock-објектима. На iOS-у, ObservableObject се тестира кроз XCTest без покретања апликације, што даје стабилност и брзину извршавања тестова.
// Unit-тест Android ViewModel-а са MockK-ом
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
iOS ViewModel се тестира аналогно: убризгавамо mock UserService, позивамо loadUser, проверавамо стање кроз XCTestExpectation. Combine Publisher се тестира кроз XCTestCase са wait(for: expectations, timeout: 1.0). Структура UserState — enum са associated values — омогућава проверу тачног стања екрана након операције.
Покривеност кода у пројектима IT Sectr-а на MVVM-у износи 75–90% за ViewModel и Repository. ViewModel је покривен unit-тестовима, Repository — интеграционим тестовима са тестном базом података. View у SwiftUI-ју и Jetpack Compose-у се тестира UI-тестовима (XCUITest, Compose Test) за критичне сценарије. Остали UI се проверава скриншот тестовима (Snapshot Testing) — то је брже од UI-тестова и даје 95% сигурности у исправност приказа.
Често постављана питања
У MVVM-у, ViewModel нема референцу на View — подаци се преносе кроз реактивне механизме (LiveData, StateFlow, @Published). У MVP-у, Presenter директно позива методе View-а кроз интерфејс ViewContract. MVVM елиминише ViewContract и ручно attach/detach, али захтева разумевање реактивних токова. ViewModel преживљава ротацију екрана на Android-у, Presenter захтева retain-фрагмент.
Минимални сет: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx или kotlinx-coroutines-core (StateFlow). За убризгавање — Hilt или Koin. За асинхроне операције — Kotlin Coroutines. За сложено повезивање података — DataBinding. У Jetpack Compose-у (који Google препоручује од 2022) довољни су compose-runtime и lifecycle-viewmodel-compose.
SwiftUI (2019) је пројектован за реактивну архитектуру: @State и @Published аутоматски прецртавају View при промени података. MVVM — природно уклапање са SwiftUI-јем: View — @ViewBuilder, ViewModel — ObservableObject. Apple не намеће MVVM као једини шаблон, али сви образовни материјали од 2019. године користе ViewModel + SwiftUI. За UIKit, Apple препоручује MVC или Coordinator.
Android: viewModelScope аутоматски отказује корутине при чишћењу ViewModel-а. iOS: AnyCancellable из Combine-а се аутоматски одјављује при ослобађању објекта који га чува. SwiftUI @StateObject управља животним циклусом аутоматски. Главна правила: не чувати референце на View/Context у ViewModel-у, отказати дуготрајне операције при чишћењу, користити weak self у затварањима.
StateFlow — савремени избор. LiveData је једноставнији и безбедан за животни циклус, али StateFlow је моћнији: ради са корутинама, подржава flatMap, combine, filter, не захтева @Nullable анотацију. Једини сценарио где је LiveData пожељнији — рад са Java кодом, где StateFlow (Kotlin Flow-API) није доступан. Google препоручује StateFlow за нове пројекте у Kotlin-у.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође