viewModelScope — είναι ένα ενσωματωμένο CoroutineScope από τη βιβλιοθήκη androidx.lifecycle, το οποίο είναι συνδεδεμένο με τον κύκλο ζωής του ViewModel και ακυρώνεται αυτόματα κατά τον καθαρισμό του. Σύμφωνα με το Google Android Developers, 2025, το viewModelScope είναι ο τυπικός μηχανισμός εκκίνησης coroutine στην αρχιτεκτονική MVVM, παρέχοντας ασφαλή εργασία με ασύγχρονες λειτουργίες χωρίς κίνδυνο διαρροής μνήμης. Το ViewModelScope χρησιμοποιεί προεπιλεγμένα το Dispatchers.Main και όλες οι IO λειτουργίες εντός του πρέπει να εκτελούνται μέσω withContext.
Κύρια σημεία
viewModelScope — είναι μια ιδιότητα επέκτασης (extension property) στη διεπαφή ViewModel, που προστέθηκε στη βιβλιοθήκη lifecycle-viewmodel-ktx (από την έκδοση 2.1.0). Παρέχει ένα έτοιμο CoroutineScope συνδεδεμένο με τον κύκλο ζωής του ViewModel.
// Εσωτερική δομή (απλοποιημένη)
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.
Όταν το ViewModel εγκαταλείπει τον κύκλο ζωής (Activity ολοκληρώθηκε ή Fragment αφαιρέθηκε), το σύστημα καλεί clear(), το οποίο ενεργοποιεί το onCleared(). Σε αυτό το callback, το viewModelScope ακυρώνει το Job του, το οποίο τερματίζει αναδρομικά όλα τα ενεργά coroutine. Ο μηχανισμός υλοποιείται μέσω της διεπαφής Closeable, όπου το Job του scope καταχωρείται ως πόρος για αυτόματο κλείσιμο.
Ο μηχανισμός σύνδεσης του viewModelScope με τον κύκλο ζωής του ViewModel βασίζεται στην επισήμανση και το callback onCleared. Ας δούμε βήμα προς βήμα πώς λειτουργεί.
Όταν το ViewModel εκτελεί viewModelScope.launch { ... }, ο getter ελέγχει αν υπάρχει αποθηκευμένο scope με την ετικέτα JOB_KEY. Αν το scope δεν έχει δημιουργηθεί ακόμα — δημιουργείται μια νέα παρουσία CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Το scope αποθηκεύεται εντός του ViewModel μέσω ενός εσωτερικού χάρτη ετικετών.
Όλα τα coroutine που εκκινούνται μέσω viewModelScope.launch ή viewModelScope.async γίνονται θυγατρικά του SupervisorJob του scope. Λειτουργούν στο κύριο νήμα (αν δεν έχει οριστεί άλλος dispatcher μέσω withContext). Όσο το ViewModel είναι ζωντανό — τα coroutine μπορεί να είναι ενεργά, σε αναστολή ή ολοκληρωμένα.
Όταν το σύστημα καταστρέφει το ViewModel, καλείται ViewModel.clear(). Εντός του clear() συμβαίνει το εξής:
Κατά την περιστροφή οθόνης, το Activity αναδημιουργείται, αλλά το ViewModel διατηρείται (χάρη στο ViewModelStoreOwner). Αυτό σημαίνει ότι το viewModelScope παραμένει ενεργό και τα coroutine συνεχίζουν να εκτελούνται χωρίς διακοπή. Μετά την αναδημιουργία του Activity, το ίδιο ViewModel (και το ίδιο scope) χρησιμοποιείται ξανά — η φόρτωση δεδομένων δεν ξεκινά από την αρχή.
MVVM (Model-View-ViewModel) — είναι η συνιστώμενη από την Google αρχιτεκτονική για εφαρμογές Android. Το viewModelScope καταλαμβάνει κεντρική θέση σε αυτήν ως εκτελεστής ασύγχρονων λειτουργιών.
| Επίπεδο | Στοιχείο | Ρόλος viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Παρατηρεί StateFlow/LiveData από το ViewModel |
| ViewModel | ViewModel | Εκκινεί coroutine μέσω viewModelScope, διαχειρίζεται κατάσταση UI |
| Repository | Repository | Λαμβάνει suspend-συναρτήσεις που καλούνται από coroutine viewModelScope |
| Data | DAO / Api | Εκτελεί πραγματικά αιτήματα (Room, Retrofit) |
Το ViewModel μέσω του viewModelScope εκκινεί coroutine, εντός των οποίων καλεί suspend-συναρτήσεις του Repository. Το αποτέλεσμα μετατρέπεται σε StateFlow, το οποίο παρατηρείται από το επίπεδο UI. Αυτό το σχήμα διασφαλίζει σαφή διαχωρισμό ευθυνών και δοκιμασιμότητα κάθε επιπέδου ανεξάρτητα.
Αν τα coroutine εκκινούνταν από το Fragment, κατά την περιστροφή οθόνης θα ακυρώνονταν μαζί με την καταστροφή του Fragment. Το ViewModel επιβιώνει της περιστροφής, επομένως τα coroutine που εκκινούνται στο scope του συνεχίζουν την εκτέλεση. Αυτό είναι το βασικό πλεονέκτημα του viewModelScope έναντι του lifecycleScope κατά τη φόρτωση δεδομένων.
Ας δούμε τρία πρακτικά σενάρια χρήσης του viewModelScope σε μια εφαρμογή Android σε 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 δεν ασχολείται με εναλλαγή νημάτων.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
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 και αντιδρά μόνο στην τρέχουσα κατάσταση, αγνοώντας παλιές κλήσεις σε επαναλαμβανόμενες περιστροφές.
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 παύση στην είσοδο. Αυτό μειώνει το φόρτο του διακομιστή και αποτρέπει τα παλιά αποτελέσματα.
Και τα δύο scope παρέχονται από τη βιβλιοθήκη AndroidX Lifecycle, αλλά είναι συνδεδεμένα με διαφορετικούς κύκλους ζωής. Η επιλογή μεταξύ τους εξαρτάται από τον τύπο εργασίας.
| Χαρακτηριστικό | viewModelScope | lifecycleScope |
|---|---|---|
| Ιδιοκτήτης | ViewModel | LifecycleOwner (Activity/Fragment) |
| Ακύρωση σε περιστροφή | Όχι (ViewModel διατηρείται) | Ναι (Activity αναδημιουργείται) |
| Προεπιλεγμένος dispatcher | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Διαθέσιμο σε | ViewModel | Activity, Fragment, Service |
| Τυπικό σενάριο | Φόρτωση δεδομένων, επιχειρηματική λογική | Αλληλεπίδραση UI, κινούμενα σχέδια, snackbar |
Η Google συνιστά τη χρήση viewModelScope για όλες τις εργασίες που σχετίζονται με φόρτωση και επεξεργασία δεδομένων. Το lifecycleScope πρέπει να εφαρμόζεται για λειτουργίες συνδεδεμένες με μια συγκεκριμένη στιγμή της ζωής του UI — για παράδειγμα, εκκίνηση κινούμενου σχεδίου κατά την πρώτη εμφάνιση της οθόνης ή εγγραφή σε ενημερώσεις Location που πρέπει να σταματήσουν κατά την αποχώρηση από την οθόνη.
Ακόμα και στο καλά τεκμηριωμένο API του Android, οι προγραμματιστές κάνουν χαρακτηριστικά λάθη. Ας δούμε τέσσερα συνηθισμένα προβλήματα.
Το πιο ύπουλο λάθος — προσπάθεια ενημέρωσης StateFlow ή LiveData μετά τον καθαρισμό του ViewModel. Αν και το viewModelScope ακυρώνεται στο onCleared(), το coroutine μπορεί να εκτελέσει κώδικα μέχρι τη στιγμή της πραγματικής ακύρωσης. Χρησιμοποιήστε isActive για έλεγχο ή βασιστείτε στην ολοκλήρωση του μπλοκ catch.
Το viewModelScope εσωτερικά χρησιμοποιεί SupervisorJob, το οποίο απομονώνει σφάλματα μεταξύ coroutine. Αλλά αν εκκινήσετε ένα coroutine με δικό του Job() εντός viewModelScope.launch, αυτό το coroutine γίνεται θυγατρικό του SupervisorJob, αλλά δεν θα προστατεύεται από ακύρωση σε σφάλματα άλλων coroutine.
Αν και το viewModelScope δεν έχει αυστηρό όριο, χιλιάδες ενεργά coroutine μπορούν να επιβραδύνουν το σύστημα. Για μεγάλες λίστες δεδομένων, χρησιμοποιήστε Flow με collectLatest αντί να δημιουργείτε ξεχωριστά coroutine για κάθε στοιχείο.
Αν κατά λάθος εισαγάγετε GlobalScope αντί για viewModelScope, το coroutine δεν θα ακυρωθεί κατά τον καθαρισμό του ViewModel. Αυτό οδηγεί σε διαρροή μνήμης και πιθανή κατάρρευση. Πάντα ελέγχετε ότι τα coroutine εκκινούνται μέσω viewModelScope, ειδικά σε κληρονομημένα fragment.
Συχνές Ερωτήσεις
Ο dispatcher του viewModelScope δεν μπορεί να αλλάξει άμεσα — είναι σταθερά ορισμένος ως Dispatchers.Main.immediate. Αλλά εντός του coroutine μπορείτε να μεταβείτε σε άλλο dispatcher μέσω withContext. Για αλλαγή dispatcher σε δοκιμές, χρησιμοποιήστε TestDispatcher μέσω Rule.
Μην μεταβιβάζετε το scope στο Repository — αυτό παραβιάζει τις αρχιτεκτονικές αρχές. Το Repository πρέπει να παρέχει suspend-συναρτήσεις και το ViewModel διαχειρίζεται μόνο του τα coroutine μέσω viewModelScope. Αν το Repository απαιτεί scope — αναθεωρήστε την αρχιτεκτονική υπέρ της Clean Architecture.
Το SupervisorJob εγγυάται ότι μια εξαίρεση σε ένα coroutine (για παράδειγμα, σφάλμα φόρτωσης ενός από πολλά ανεξάρτητα αιτήματα) δεν ακυρώνει τα άλλα coroutine. Αυτό αντιστοιχεί στο σενάριο ViewModel, όπου διαφορετικές οθόνες φορτώνουν ανεξάρτητα δεδομένα.
Ναι, το viewModelScope είναι διαθέσιμο σε οποιοδήποτε ViewModel, ανεξάρτητα από τον τύπο UI (View System ή Jetpack Compose). Στο Compose, τα coroutine επίσης εκκινούνται μέσω viewModelScope και για εφέ UI χρησιμοποιούνται LaunchedEffect και rememberCoroutineScope.
Η κλήση viewModelScope.cancel() ακυρώνει το scope αμέσως — όλα τα ενεργά coroutine τερματίζονται με CancellationException. Αν μετά από αυτό καλέσετε viewModelScope.launch, ένα νέο scope δημιουργείται αυτόματα στην επόμενη πρόσβαση στον getter.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης