viewModelScope: τι είναι, σύνδεση με ViewModel και λειτουργία στο Android

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

viewModelScope — είναι ένα ενσωματωμένο CoroutineScope από τη βιβλιοθήκη androidx.lifecycle, το οποίο είναι συνδεδεμένο με τον κύκλο ζωής του ViewModel και ακυρώνεται αυτόματα κατά τον καθαρισμό του. Σύμφωνα με το Google Android Developers, 2025, το viewModelScope είναι ο τυπικός μηχανισμός εκκίνησης coroutine στην αρχιτεκτονική MVVM, παρέχοντας ασφαλή εργασία με ασύγχρονες λειτουργίες χωρίς κίνδυνο διαρροής μνήμης. Το ViewModelScope χρησιμοποιεί προεπιλεγμένα το Dispatchers.Main και όλες οι IO λειτουργίες εντός του πρέπει να εκτελούνται μέσω withContext.

Κύρια σημεία

  • viewModelScope — CoroutineScope από το lifecycle-viewmodel-ktx, ακυρώνεται στο onCleared() του ViewModel
  • Dispatchers.Main — προεπιλεγμένος dispatcher, επομένως οι ενημερώσεις UI εντός coroutine είναι ασφαλείς
  • onCleared — callback, κατά την κλήση του οποίου το viewModelScope ακυρώνει αυτόματα όλα τα ενεργά coroutine
  • clear() vs onCleared() — το clear() καλείται από το framework πριν από το onCleared, διασφαλίζοντας την ακύρωση του scope
  • launch — ο κύριος τρόπος εκκίνησης coroutine στο viewModelScope για λειτουργίες fire-and-forget

Τι είναι το viewModelScope στο Android;

viewModelScope — είναι μια ιδιότητα επέκτασης (extension property) στη διεπαφή ViewModel, που προστέθηκε στη βιβλιοθήκη lifecycle-viewmodel-ktx (από την έκδοση 2.1.0). Παρέχει ένα έτοιμο CoroutineScope συνδεδεμένο με τον κύκλο ζωής του ViewModel.

kotlin
// Εσωτερική δομή (απλοποιημένη)
val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope = this.getTag(JOB_KEY)
        if (scope != null) return scope
        return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
            .also { setTag(JOB_KEY, it) }
    }

Το scope δημιουργείται τεμπέλικα (lazy) κατά την πρώτη πρόσβαση και αποθηκεύεται στην προσωρινή μνήμη μέσω setTag. Χρησιμοποιείται SupervisorJob, που σημαίνει ότι μια εξαίρεση σε ένα θυγατρικό coroutine δεν ακυρώνει τα άλλα. Ο προεπιλεγμένος dispatcher είναι Dispatchers.Main.immediate, ο οποίος εκτελεί κώδικα στο κύριο νήμα χωρίς πρόσθετη δρομολόγηση αν η κλήση είναι ήδη στο Main.

Πώς λαμβάνει το viewModelScope ειδοποίηση καθαρισμού

Όταν το ViewModel εγκαταλείπει τον κύκλο ζωής (Activity ολοκληρώθηκε ή Fragment αφαιρέθηκε), το σύστημα καλεί clear(), το οποίο ενεργοποιεί το onCleared(). Σε αυτό το callback, το viewModelScope ακυρώνει το Job του, το οποίο τερματίζει αναδρομικά όλα τα ενεργά coroutine. Ο μηχανισμός υλοποιείται μέσω της διεπαφής Closeable, όπου το Job του scope καταχωρείται ως πόρος για αυτόματο κλείσιμο.

Πώς λειτουργεί το viewModelScope: σύνδεση με τον κύκλο ζωής του ViewModel

Ο μηχανισμός σύνδεσης του viewModelScope με τον κύκλο ζωής του ViewModel βασίζεται στην επισήμανση και το callback onCleared. Ας δούμε βήμα προς βήμα πώς λειτουργεί.

Βήμα 1: Δημιουργία scope κατά την πρώτη πρόσβαση

Όταν το ViewModel εκτελεί viewModelScope.launch { ... }, ο getter ελέγχει αν υπάρχει αποθηκευμένο scope με την ετικέτα JOB_KEY. Αν το scope δεν έχει δημιουργηθεί ακόμα — δημιουργείται μια νέα παρουσία CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Το scope αποθηκεύεται εντός του ViewModel μέσω ενός εσωτερικού χάρτη ετικετών.

Βήμα 2: Κύκλος ζωής coroutine

Όλα τα coroutine που εκκινούνται μέσω viewModelScope.launch ή viewModelScope.async γίνονται θυγατρικά του SupervisorJob του scope. Λειτουργούν στο κύριο νήμα (αν δεν έχει οριστεί άλλος dispatcher μέσω withContext). Όσο το ViewModel είναι ζωντανό — τα coroutine μπορεί να είναι ενεργά, σε αναστολή ή ολοκληρωμένα.

Βήμα 3: Ακύρωση στο onCleared

Όταν το σύστημα καταστρέφει το ViewModel, καλείται ViewModel.clear(). Εντός του clear() συμβαίνει το εξής:

  • Κλήση onCleared() για τη λογική χρήστη
  • Κλείσιμο όλων των πόρων Closeable που καταχωρήθηκαν μέσω addCloseable
  • Το Job του viewModelScope μεταβαίνει σε κατάσταση Cancelled
  • Όλα τα θυγατρικά coroutine ακυρώνονται αναδρομικά
  • Απελευθέρωση αναφορών στο scope για τον συλλέκτη σκουπιδιών

Αντοχή στην περιστροφή οθόνης

Κατά την περιστροφή οθόνης, το Activity αναδημιουργείται, αλλά το ViewModel διατηρείται (χάρη στο ViewModelStoreOwner). Αυτό σημαίνει ότι το viewModelScope παραμένει ενεργό και τα coroutine συνεχίζουν να εκτελούνται χωρίς διακοπή. Μετά την αναδημιουργία του Activity, το ίδιο ViewModel (και το ίδιο scope) χρησιμοποιείται ξανά — η φόρτωση δεδομένων δεν ξεκινά από την αρχή.

viewModelScope στην αρχιτεκτονική MVVM

MVVM (Model-View-ViewModel) — είναι η συνιστώμενη από την Google αρχιτεκτονική για εφαρμογές Android. Το viewModelScope καταλαμβάνει κεντρική θέση σε αυτήν ως εκτελεστής ασύγχρονων λειτουργιών.

Ο ρόλος του viewModelScope στα επίπεδα αρχιτεκτονικής

ΕπίπεδοΣτοιχείοΡόλος viewModelScope
UIActivity / FragmentΠαρατηρεί StateFlow/LiveData από το ViewModel
ViewModelViewModelΕκκινεί coroutine μέσω viewModelScope, διαχειρίζεται κατάσταση UI
RepositoryRepositoryΛαμβάνει suspend-συναρτήσεις που καλούνται από coroutine viewModelScope
DataDAO / ApiΕκτελεί πραγματικά αιτήματα (Room, Retrofit)

Το ViewModel μέσω του viewModelScope εκκινεί coroutine, εντός των οποίων καλεί suspend-συναρτήσεις του Repository. Το αποτέλεσμα μετατρέπεται σε StateFlow, το οποίο παρατηρείται από το επίπεδο UI. Αυτό το σχήμα διασφαλίζει σαφή διαχωρισμό ευθυνών και δοκιμασιμότητα κάθε επιπέδου ανεξάρτητα.

Γιατί viewModelScope στο ViewModel, όχι στο Fragment

Αν τα coroutine εκκινούνταν από το Fragment, κατά την περιστροφή οθόνης θα ακυρώνονταν μαζί με την καταστροφή του Fragment. Το ViewModel επιβιώνει της περιστροφής, επομένως τα coroutine που εκκινούνται στο scope του συνεχίζουν την εκτέλεση. Αυτό είναι το βασικό πλεονέκτημα του viewModelScope έναντι του lifecycleScope κατά τη φόρτωση δεδομένων.

Παραδείγματα χρήσης του viewModelScope

Ας δούμε τρία πρακτικά σενάρια χρήσης του viewModelScope σε μια εφαρμογή Android σε Kotlin.

Παράδειγμα 1: Φόρτωση δεδομένων κατά τη δημιουργία ViewModel

kotlin
class ProfileViewModel(
    private val repo: ProfileRepository
) : ViewModel() {

    private val _profile = MutableStateFlow<Profile?>(null)
    val profile: StateFlow<Profile?> = _profile

    init {
        loadProfile()
    }

    private fun loadProfile() {
        viewModelScope.launch {
            val result = repo.getProfile()
            _profile.value = result
        }
    }
}

Στο μπλοκ init ξεκινά αμέσως η φόρτωση του προφίλ. Το coroutine εκτελείται στο κύριο νήμα (προεπιλεγμένα). Το repository χρησιμοποιεί withContext(Dispatchers.IO) για το αίτημα δικτύου εντός της suspend-συνάρτησής του, επομένως το ViewModel δεν ασχολείται με εναλλαγή νημάτων.

Παράδειγμα 2: Διαχείριση σφαλμάτων μέσω sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
fun fetchItems() {
    _state.value = UiState.Loading
    viewModelScope.launch {
        try {
            val items = repo.getItems()
            _state.value = UiState.Success(items)
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "Unknown error")
        }
    }
}

Η κατάσταση UI περιγράφεται μέσω sealed class UiState. Το ViewModel ενημερώνει την κατάσταση σε κάθε αλλαγή. Το Fragment είναι συνδρομημένο στο StateFlow και αντιδρά μόνο στην τρέχουσα κατάσταση, αγνοώντας παλιές κλήσεις σε επαναλαμβανόμενες περιστροφές.

Παράδειγμα 3: Ακύρωση προηγούμενου coroutine σε νέο αίτημα

kotlin
private var searchJob: Job? = null

fun search(query: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(300)
        val results = repo.search(query)
        _searchResults.value = results
    }
}

Σε κάθε νέο αίτημα αναζήτησης, το προηγούμενο coroutine ακυρώνεται. Το delay(300) υλοποιεί debounce — η αναζήτηση εκτελείται μόνο μετά από 300 ms παύση στην είσοδο. Αυτό μειώνει το φόρτο του διακομιστή και αποτρέπει τα παλιά αποτελέσματα.

viewModelScope vs lifecycleScope: πότε να επιλέξουμε τι

Και τα δύο scope παρέχονται από τη βιβλιοθήκη AndroidX Lifecycle, αλλά είναι συνδεδεμένα με διαφορετικούς κύκλους ζωής. Η επιλογή μεταξύ τους εξαρτάται από τον τύπο εργασίας.

Σύγκριση scope

ΧαρακτηριστικόviewModelScopelifecycleScope
ΙδιοκτήτηςViewModelLifecycleOwner (Activity/Fragment)
Ακύρωση σε περιστροφήΌχι (ViewModel διατηρείται)Ναι (Activity αναδημιουργείται)
Προεπιλεγμένος dispatcherDispatchers.Main.immediateDispatchers.Main.immediate
Διαθέσιμο σεViewModelActivity, Fragment, Service
Τυπικό σενάριοΦόρτωση δεδομένων, επιχειρηματική λογικήΑλληλεπίδραση UI, κινούμενα σχέδια, snackbar

Συστάσεις Google

Η Google συνιστά τη χρήση viewModelScope για όλες τις εργασίες που σχετίζονται με φόρτωση και επεξεργασία δεδομένων. Το lifecycleScope πρέπει να εφαρμόζεται για λειτουργίες συνδεδεμένες με μια συγκεκριμένη στιγμή της ζωής του UI — για παράδειγμα, εκκίνηση κινούμενου σχεδίου κατά την πρώτη εμφάνιση της οθόνης ή εγγραφή σε ενημερώσεις Location που πρέπει να σταματήσουν κατά την αποχώρηση από την οθόνη.

Συνήθη λάθη κατά την εργασία με το viewModelScope

Ακόμα και στο καλά τεκμηριωμένο API του Android, οι προγραμματιστές κάνουν χαρακτηριστικά λάθη. Ας δούμε τέσσερα συνηθισμένα προβλήματα.

Λάθος 1: Ενημέρωση UI μετά την ακύρωση scope

Το πιο ύπουλο λάθος — προσπάθεια ενημέρωσης StateFlow ή LiveData μετά τον καθαρισμό του ViewModel. Αν και το viewModelScope ακυρώνεται στο onCleared(), το coroutine μπορεί να εκτελέσει κώδικα μέχρι τη στιγμή της πραγματικής ακύρωσης. Χρησιμοποιήστε isActive για έλεγχο ή βασιστείτε στην ολοκλήρωση του μπλοκ catch.

Λάθος 2: Εκκίνηση coroutine χωρίς να λαμβάνεται υπόψη το SupervisorJob

Το viewModelScope εσωτερικά χρησιμοποιεί SupervisorJob, το οποίο απομονώνει σφάλματα μεταξύ coroutine. Αλλά αν εκκινήσετε ένα coroutine με δικό του Job() εντός viewModelScope.launch, αυτό το coroutine γίνεται θυγατρικό του SupervisorJob, αλλά δεν θα προστατεύεται από ακύρωση σε σφάλματα άλλων coroutine.

Λάθος 3: Υπερβολικά πολλά coroutine σε ένα scope

Αν και το viewModelScope δεν έχει αυστηρό όριο, χιλιάδες ενεργά coroutine μπορούν να επιβραδύνουν το σύστημα. Για μεγάλες λίστες δεδομένων, χρησιμοποιήστε Flow με collectLatest αντί να δημιουργείτε ξεχωριστά coroutine για κάθε στοιχείο.

Λάθος 4: Χρήση GlobalScope αντί για viewModelScope

Αν κατά λάθος εισαγάγετε GlobalScope αντί για viewModelScope, το coroutine δεν θα ακυρωθεί κατά τον καθαρισμό του ViewModel. Αυτό οδηγεί σε διαρροή μνήμης και πιθανή κατάρρευση. Πάντα ελέγχετε ότι τα coroutine εκκινούνται μέσω viewModelScope, ειδικά σε κληρονομημένα fragment.

Συχνές Ερωτήσεις

Μπορεί να αλλάξει ο προεπιλεγμένος dispatcher του viewModelScope;

Ο dispatcher του viewModelScope δεν μπορεί να αλλάξει άμεσα — είναι σταθερά ορισμένος ως Dispatchers.Main.immediate. Αλλά εντός του coroutine μπορείτε να μεταβείτε σε άλλο dispatcher μέσω withContext. Για αλλαγή dispatcher σε δοκιμές, χρησιμοποιήστε TestDispatcher μέσω Rule.

Πώς να μεταβιβάσετε το viewModelScope στο Repository;

Μην μεταβιβάζετε το scope στο Repository — αυτό παραβιάζει τις αρχιτεκτονικές αρχές. Το Repository πρέπει να παρέχει suspend-συναρτήσεις και το ViewModel διαχειρίζεται μόνο του τα coroutine μέσω viewModelScope. Αν το Repository απαιτεί scope — αναθεωρήστε την αρχιτεκτονική υπέρ της Clean Architecture.

Γιατί το viewModelScope χρησιμοποιεί SupervisorJob;

Το SupervisorJob εγγυάται ότι μια εξαίρεση σε ένα coroutine (για παράδειγμα, σφάλμα φόρτωσης ενός από πολλά ανεξάρτητα αιτήματα) δεν ακυρώνει τα άλλα coroutine. Αυτό αντιστοιχεί στο σενάριο ViewModel, όπου διαφορετικές οθόνες φορτώνουν ανεξάρτητα δεδομένα.

Είναι διαθέσιμο το viewModelScope στο Jetpack Compose;

Ναι, το viewModelScope είναι διαθέσιμο σε οποιοδήποτε ViewModel, ανεξάρτητα από τον τύπο UI (View System ή Jetpack Compose). Στο Compose, τα coroutine επίσης εκκινούνται μέσω viewModelScope και για εφέ UI χρησιμοποιούνται LaunchedEffect και rememberCoroutineScope.

Τι συμβαίνει με το coroutine κατά την κλήση viewModelScope.cancel();

Η κλήση viewModelScope.cancel() ακυρώνει το scope αμέσως — όλα τα ενεργά coroutine τερματίζονται με CancellationException. Αν μετά από αυτό καλέσετε viewModelScope.launch, ένα νέο scope δημιουργείται αυτόματα στην επόμενη πρόσβαση στον getter.

Σύνοψη

  • viewModelScope — CoroutineScope συνδεδεμένο με τον κύκλο ζωής ViewModel, ακυρώνεται αυτόματα στο onCleared()
  • SupervisorJob + Dispatchers.Main — εσωτερική διαμόρφωση που διασφαλίζει απομόνωση σφαλμάτων και ασφαλή πρόσβαση στο UI
  • Περιστροφή οθόνης — το ViewModel διατηρείται, επομένως τα coroutine στο viewModelScope συνεχίζουν χωρίς επανεκκίνηση
  • Αρχιτεκτονική MVVM — το viewModelScope είναι το κεντρικό στοιχείο για ασύγχρονες λειτουργίες στο επίπεδο ViewModel
  • lifecycleScope — εναλλακτική για λειτουργίες συνδεδεμένες με τον κύκλο ζωής Activity/Fragment, όχι ViewModel
  • StateFlow — προτιμώμενος τρόπος μεταφοράς δεδομένων από coroutine viewModelScope στο UI μέσω sealed class
  • GlobalScope είναι επικίνδυνο — η αντικατάσταση viewModelScope με GlobalScope οδηγεί σε διαρροές μνήμης και καταρρεύσεις εφαρμογής

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

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

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

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