MVVM: τι είναι, το μοτίβο Model-View-ViewModel στην ανάπτυξη εφαρμογών για κινητά

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-16 Χρόνος ανάγνωσης: 9 λεπ

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 (κατάσταση και λογική χωρίς αναφορά στο View)
  • Reactive binding — LiveData, StateFlow, ObservableObject ενημερώνουν αυτόματα το UI όταν αλλάζουν τα δεδομένα
  • ViewModel — επιβιώνει από την περιστροφή οθόνης και δεν εξαρτάται από Android SDK/UIKit, δοκιμάζεται με unit-tests
  • Android Jetpack — ViewModel, LiveData, DataBinding — επίσημη στοίβα από την Google για MVVM
  • SwiftUI + Combine — εγγενής υλοποίηση MVVM σε iOS με @Published και @ObservedObject

Τι είναι το MVVM: ουσία του μοτίβου Model-View-ViewModel

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Εμφάνιση, εγγραφή στο ViewModelActivity/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.

MVVM στο Android: ViewModel, LiveData και StateFlow

ViewModel από το Jetpack — το επίσημο στοιχείο από την Google για την υλοποίηση MVVM. Το ViewModel επιβιώνει από την περιστροφή οθόνης: όταν αλλάζει η διαμόρφωση, το Activity καταστρέφεται και αναδημιουργείται, ενώ το ViewModel παραμένει στη μνήμη. Το νέο στιγμιότυπο Activity λαμβάνει το ίδιο ViewModel μέσω του ViewModelProvider. Το ViewModel δεν έχει αναφορές σε Activity, Context ή View — είναι καθαρό και δοκιμάζεται με unit-tests χωρίς Robolectric.

kotlin
// 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 και SwiftUI

MVVM στο iOS υλοποιείται μέσω ObservableObject από το Combine. Το ViewModel — μια κλάση που κληρονομεί το ObservableObject, με πεδία @Published. Το SwiftUI View εγγράφεται στο ViewModel μέσω @ObservedObject ή @StateObject. Όταν αλλάζει μια ιδιότητα @Published, το SwiftUI ανασχεδιάζει αυτόματα το View που εξαρτάται από αυτήν την ιδιότητα. Η Apple παρουσίασε το SwiftUI το 2019 στο WWDC μαζί με το Combine — από εκείνη τη στιγμή το MVVM έγινε το επίσημα προτεινόμενο μοτίβο για iOS.

swift
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: πλεονεκτήματα και μειονεκτήματα

Το MVVM υπερτερεί του MVP σε τρεις βασικές πτυχές: απουσία διεπαφών ViewContract, αυτόματη διαχείριση συνδρομών και επιβίωση από περιστροφή οθόνης. Στο MVP, κάθε οθόνη απαιτεί μια διεπαφή ViewContract + κλάση Presenter + εγγραφή/απεγγραφή σε onStart/onStop. Στο MVVM δημιουργείται μόνο το ViewModel — η εγγραφή στο Activity γίνεται μέσω observe() χωρίς χειροκίνητο detach().

ΚριτήριοMVPMVVM
Διεπαφές ViewContract1 ανά οθόνηΔεν χρειάζονται
Διαχείριση συνδρομώνΧειροκίνητο attach/detachΑυτόματη (lifecycle-aware)
Περιστροφή οθόνηςRetain-fragmentViewModel επιβιώνει
ΔοκιμήMock ViewContractΚαθαρή κλάση χωρίς εξαρτήσεις
ΑντιδραστικότηταCallbacks στο PresenterLiveData/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 σε Android και iOS

Το ViewModel δοκιμάζεται με unit-tests χωρίς εξαρτήσεις πλατφόρμας — αυτό είναι το κύριο επιχείρημα υπέρ του MVVM. Στο Android, το ViewModel δεν περιέχει Activity, Context ή View — όλες οι εξαρτήσεις (Repository, UseCase) μεταβιβάζονται μέσω κατασκευαστή και αντικαθίστανται με mock αντικείμενα. Στο iOS, το ObservableObject δοκιμάζεται μέσω XCTest χωρίς εκκίνηση της εφαρμογής, γεγονός που παρέχει σταθερότητα και ταχύτητα εκτέλεσης δοκιμών.

kotlin
// 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 και MVP;

Στο MVVM, το ViewModel δεν έχει αναφορά στο View — τα δεδομένα μεταδίδονται μέσω αντιδραστικών μηχανισμών (LiveData, StateFlow, @Published). Στο MVP, το Presenter καλεί απευθείας μεθόδους του View μέσω της διεπαφής ViewContract. Το MVVM εξαλείφει το ViewContract και το χειροκίνητο attach/detach, αλλά απαιτεί κατανόηση των αντιδραστικών ροών. Το ViewModel επιβιώνει από περιστροφή οθόνης στο Android, το Presenter απαιτεί retain-fragment.

Ποιες βιβλιοθήκες χρειάζονται για MVVM στο Android;

Ελάχιστο σύνολο: 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.

Γιατί η Apple προτείνει MVVM για iOS;

Το SwiftUI (2019) σχεδιάστηκε για αντιδραστική αρχιτεκτονική: τα @State και @Published ανασχεδιάζουν αυτόματα το View όταν αλλάζουν τα δεδομένα. Το MVVM — φυσική αντιστοιχία για SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Η Apple δεν επιβάλλει το MVVM ως μοναδικό μοτίβο, αλλά όλο το εκπαιδευτικό υλικό από το 2019 χρησιμοποιεί ViewModel + SwiftUI. Για UIKit, η Apple προτείνει MVC ή Coordinator.

Πώς να αποφύγετε διαρροές μνήμης στο ViewModel;

Android: το viewModelScope ακυρώνει αυτόματα τα coroutines κατά τον καθαρισμό του ViewModel. iOS: το AnyCancellable από το Combine απεγγράφεται αυτόματα κατά την απελευθέρωση του αντικειμένου που το αποθηκεύει. Το SwiftUI @StateObject διαχειρίζεται τον κύκλο ζωής αυτόματα. Βασικοί κανόνες: μην αποθηκεύετε αναφορές σε View/Context στο ViewModel, ακυρώνετε μακροχρόνιες λειτουργίες κατά τον καθαρισμό, χρησιμοποιείτε weak self στα closures.

Τι να επιλέξετε: LiveData ή StateFlow για Android;

StateFlow — η σύγχρονη επιλογή. Το LiveData είναι απλούστερο και ασφαλές για τον κύκλο ζωής, αλλά το StateFlow είναι πιο ισχυρό: λειτουργεί με coroutines, υποστηρίζει flatMap, combine, filter, δεν απαιτεί σημείωση @Nullable. Το μοναδικό σενάριο όπου το LiveData είναι προτιμότερο — εργασία με κώδικα Java, όπου το StateFlow (Kotlin Flow-API) δεν είναι διαθέσιμο. Η Google προτείνει StateFlow για νέα έργα σε Kotlin.

Σύνοψη

  • MVVM (Model-View-ViewModel) — αντιδραστικό μοτίβο, όπου το ViewModel δεν έχει αναφορά στο View
  • ViewModel — επιβιώνει από περιστροφή οθόνης, δοκιμάζεται με unit-tests, ανεξάρτητο από UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — σύγχρονη στοίβα από την Google
  • iOS — ObservableObject + @Published + SwiftUI — εγγενής υλοποίηση MVVM
  • MVVM vs MVP — το MVVM εξαλείφει το ViewContract και το χειροκίνητο attach/detach
  • Δοκιμή — το ViewModel καλύπτεται από unit-tests χωρίς εξαρτήσεις πλατφόρμας
  • Πρόταση — MVVM για νέα έργα; MVP για υποστήριξη legacy

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης