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-tests χωρίς 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 — είναι πιο σύντομο, πιο ισχυρό και ενσωματώνεται καλύτερα με τα coroutines.
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-fragment | ViewModel επιβιώνει |
| Δοκιμή | Mock ViewContract | Καθαρή κλάση χωρίς εξαρτήσεις |
| Αντιδραστικότητα | Callbacks στο Presenter | LiveData/StateFlow/Combine |
Μειονεκτήματα MVVM — πολυπλοκότητα εντοπισμού σφαλμάτων σε αντιδραστικές αλυσίδες και κίνδυνος διαρροής μνήμης σε λανθασμένη εγγραφή. Το LiveData λύνει το πρόβλημα ασφάλειας κύκλου ζωής, το StateFlow απαιτεί lifecycleScope, το Combine — sink με AnyCancellable. Στο MVP όλες οι κλήσεις είναι σαφείς (view.showUser), στο MVVM τα δεδομένα έρχονται μέσω αντιδραστικής ροής — η ιχνηλάτηση απαιτεί debug-breakpoints στο closure 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-tests χωρίς εξαρτήσεις πλατφόρμας — αυτό είναι το κύριο επιχείρημα υπέρ του MVVM. Στο Android, το ViewModel δεν περιέχει Activity, Context ή View — όλες οι εξαρτήσεις (Repository, UseCase) μεταβιβάζονται μέσω κατασκευαστή και αντικαθίστανται με mock αντικείμενα. Στο iOS, το ObservableObject δοκιμάζεται μέσω XCTest χωρίς εκκίνηση της εφαρμογής, γεγονός που παρέχει σταθερότητα και ταχύτητα εκτέλεσης δοκιμών.
// Unit-test 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 δοκιμάζεται παρόμοια: κάνουμε inject ένα 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-tests, το 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-fragment.
Ελάχιστο σύνολο: 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 ακυρώνει αυτόματα τα coroutines κατά τον καθαρισμό του ViewModel. iOS: το AnyCancellable από το Combine απεγγράφεται αυτόματα κατά την απελευθέρωση του αντικειμένου που το αποθηκεύει. Το SwiftUI @StateObject διαχειρίζεται τον κύκλο ζωής αυτόματα. Βασικοί κανόνες: μην αποθηκεύετε αναφορές σε View/Context στο ViewModel, ακυρώνετε μακροχρόνιες λειτουργίες κατά τον καθαρισμό, χρησιμοποιείτε weak self στα closures.
StateFlow — η σύγχρονη επιλογή. Το LiveData είναι απλούστερο και ασφαλές για τον κύκλο ζωής, αλλά το StateFlow είναι πιο ισχυρό: λειτουργεί με coroutines, υποστηρίζει flatMap, combine, filter, δεν απαιτεί σημείωση @Nullable. Το μοναδικό σενάριο όπου το LiveData είναι προτιμότερο — εργασία με κώδικα Java, όπου το StateFlow (Kotlin Flow-API) δεν είναι διαθέσιμο. Η Google προτείνει StateFlow για νέα έργα σε Kotlin.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης