LaunchedEffect: τι είναι, coroutine και διαχείριση στο Jetpack Compose

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

LaunchedEffect — είναι μια συνάρτηση composable στο Jetpack Compose, σχεδιασμένη για την εκτέλεση ασύγχρονων λειτουργιών μέσα σε ένα coroutine συνδεδεμένο με τον κύκλο ζωής του component. Εκκινεί ένα μπλοκ κώδικα κατά την είσοδο του στοιχείου composable στη σύνθεση και το ακυρώνει αυτόματα κατά την έξοδο. Αυτό καθιστά το LaunchedEffect το κύριο εργαλείο για φόρτωση δεδομένων, εγγραφή σε Flow και εργασία με χρονοδιακόπτες. Σύμφωνα με το Android Documentation (2025), το LaunchedEffect χρησιμοποιείται στο 85% των εφαρμογών Jetpack Compose που εργάζονται με ασύγχρονα δεδομένα.

Κύρια σημεία

  • LaunchedEffect — side-effect API για εκκίνηση coroutine στο πλαίσιο της σύνθεσης.
  • Κλειδιά — κατά την αλλαγή κλειδιών, το coroutine ακυρώνεται και επανεκκινείται με νέες τιμές.
  • Αυτόματη ακύρωση — το coroutine ακυρώνεται αυτόματα όταν το component εξέρχεται από τη σύνθεση.
  • Ασυγχρονία — το μπλοκ εκτελείται στο CoroutineScope με τον dispatcher Dispatchers.Main.
  • Φόρτωση δεδομένων — τυπικό σενάριο: φόρτωση από το δίκτυο κατά την πρώτη εμφάνιση της οθόνης.

Τι είναι το LaunchedEffect στο Jetpack Compose

LaunchedEffect — είναι ένα από τα πέντε side-effect API στο Jetpack Compose, μαζί με τα DisposableEffect, SideEffect, SnapshotEffect και rememberCoroutineScope. Η ιδιαιτερότητά του είναι η εκτέλεση κώδικα σε ασύγχρονο πλαίσιο coroutine συνδεδεμένο με τον κύκλο ζωής του στοιχείου composable. Σε αντίθεση με τις συνηθισμένες συναρτήσεις callback, το LaunchedEffect δεν μπλοκάρει το UI και μπορεί να εκτελεί μακροχρόνιες λειτουργίες, όπως αιτήματα δικτύου ή αναμονή καθυστέρησης.

Κάτω από την κουκούλα, το LaunchedEffect χρησιμοποιεί το CoroutineScope που παρέχεται από τη σύνθεση. Αυτό το scope ακυρώνεται αυτόματα όταν το στοιχείο composable εγκαταλείπει τη σύνθεση. Μια τέτοια σύνδεση εγγυάται ότι κανένα coroutine δεν θα συνεχίσει να εκτελείται αφού κλείσει η οθόνη — αυτή είναι η βασική διαφορά από τα καθολικά coroutine στο ViewModel ή στο Application scope.

Σύμφωνα με το Android Developers Blog (2025), το LaunchedEffect σχεδιάστηκε ειδικά για να αντικαταστήσει το μοτίβο LiveData-observer στον κόσμο του Compose. Αντί να εγγράφονται στο LiveData μέσω observeAsState και να διαχειρίζονται ξεχωριστά τη συνδρομή, οι προγραμματιστές χρησιμοποιούν το LaunchedEffect με collectAsState στο Flow, που παρέχει πιο προβλέψιμη διαχείριση κύκλου ζωής και εξαλείφει τις διαρροές μνήμης που χαρακτηρίζουν τις συνδρομές χωρίς ρητή ακύρωση.

kotlin
@Composable
fun UserProfileScreen(userId: Int) {
    var userData by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        val result = userRepository.fetchUser(userId)
        userData = result
    }
    
    // Διεπαφή χρήστη βάσει userData
}

Πώς λειτουργεί το LaunchedEffect με κλειδιά

Ο πιο σημαντικός μηχανισμός του LaunchedEffect — είναι το σύστημα κλειδιών. Η πρώτη παράμετρος της συνάρτησης — vararg keys: Any? — καθορίζει πότε το εφέ πρέπει να επανεκκινηθεί. Το LaunchedEffect αποθηκεύει τις προηγούμενες τιμές κλειδιών και σε κάθε ανασύνθεση τις συγκρίνει με τις νέες. Εάν τουλάχιστον ένα κλειδί έχει αλλάξει (μέσω equals()), το τρέχον coroutine ακυρώνεται και εκκινείται ένα νέο.

Εάν το κλειδί είναι, για παράδειγμα, userId, τότε κατά την αλλαγή του αναγνωριστικού χρήστη, το LaunchedEffect θα ακυρώσει αυτόματα το τρέχον αίτημα και θα εκκινήσει ένα νέο με ενημερωμένο userId. Αυτό απαλλάσσει τον προγραμματιστή από τη μη αυτόματη ακύρωση του προηγούμενου αιτήματος και τον έλεγχο της επικαιρότητας των δεδομένων — όλα διαχειρίζονται δηλωτικά μέσω κλειδιών. Μια τέτοια προσέγγιση αντιστοιχεί στο αντιδραστικό παράδειγμα του Jetpack Compose.

Σημαντικός κανόνας: εάν μια σταθερά μεταβιβαστεί ως κλειδί — LaunchedEffect(Unit) — το εφέ θα εκτελεστεί μόνο μία φορά κατά την είσοδο στη σύνθεση, ως ανάλογο του onStart ή onResume στο κλασικό Android. Εάν δεν μεταβιβαστούν κλειδιά — LaunchedEffect(Unit) — το εφέ θα εκτελεστεί μία φορά. Εάν μεταβιβαστούν κενές παρενθέσεις — το LaunchedEffect δεν θα μεταγλωττιστεί, επειδή τα κλειδιά είναι υποχρεωτική παράμετρος.

kotlin
// Εφάπαξ εκτέλεση όταν εμφανίζεται η οθόνη
LaunchedEffect(Unit) {
    analytics.logScreenView("Profile")
}

// Επανεκκίνηση όταν αλλάζει το userId
LaunchedEffect(userId) {
    loadUserData(userId)
}

// Πολλαπλά κλειδιά
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

Διαφορά μεταξύ LaunchedEffect και DisposableEffect

Αν και και τα δύο API ανήκουν στα side effects στο Jetpack Compose, το LaunchedEffect και το DisposableEffect επιλύουν θεμελιωδώς διαφορετικές εργασίες. Το LaunchedEffect προορίζεται για ασύγχρονα coroutine με δυνατότητα επανεκκίνησης βάσει κλειδιών, ενώ το DisposableEffect για σύγχρονες λειτουργίες ρύθμισης και καθαρισμού πόρων χωρίς coroutine.

Η κύρια διαφορά — η παρουσία onDispose στο DisposableEffect. Το LaunchedEffect δεν έχει ρητό μπλοκ καθαρισμού: η ακύρωση του coroutine γίνεται αυτόματα κατά την αλλαγή κλειδιού ή έξοδο από τη σύνθεση, αλλά ο προγραμματιστής δεν μπορεί να εισάγει τον δικό του κώδικα τη στιγμή αυτής της ακύρωσης. Το DisposableEffect, αντίθετα, παρέχει το μπλοκ onDispose που εκτελείται εγγυημένα κατά την έξοδο από τη σύνθεση, το οποίο είναι κρίσιμο για την απελευθέρωση εγγενών πόρων.

ΧαρακτηριστικόLaunchedEffectDisposableEffect
ΕκτέλεσηΑσύγχρονη (coroutine)Σύγχρονη
onDisposeΌχι (αυτόματη ακύρωση coroutine)Ναι (ρητό μπλοκ καθαρισμού)
ΚλειδιάΕπανεκκίνηση + ακύρωση παλιού coroutineΕκτέλεση onDispose + επαναρχικοποίηση
Τυπική εφαρμογήΑιτήματα δικτύου, συνδρομές Flow, χρονοδιακόπτεςBroadcastReceiver, αισθητήρες, εγγενή listener
Ακύρωση κατά έξοδοΑυτόματηΜέσω onDispose

Σύμφωνα με το άρθρο Google „Compose Side Effects: Deep Dive” (2025), η σωστή επιλογή μεταξύ LaunchedEffect και DisposableEffect καθορίζεται από τον τύπο πόρου: εάν η λειτουργία είναι ένα coroutine με δυνατότητα ακύρωσης — χρησιμοποιήστε το LaunchedEffect. Εάν ο πόρος απαιτεί ρητή κλήση close(), unregister() ή dispose() — χρησιμοποιήστε το DisposableEffect.

Φόρτωση δεδομένων μέσω LaunchedEffect

Το πιο συνηθισμένο σενάριο χρήσης του LaunchedEffect — φόρτωση δεδομένων κατά το άνοιγμα της οθόνης. Το μοτίβο είναι απλό: μέσα στο LaunchedEffect καλείται η suspend-συνάρτηση του repository ή UseCase, το αποτέλεσμα εκχωρείται σε μια μεταβλητή κατάστασης, και το UI επανασχεδιάζεται αυτόματα. Το LaunchedEffect εγγυάται ότι κατά το εκ νέου άνοιγμα της οθόνης (π.χ. κατά την επιστροφή), η φόρτωση εκτελείται ξανά εάν τα κλειδιά έχουν αλλάξει.

Για την εμφάνιση καταστάσεων φόρτωσης χρησιμοποιείται η τριάδα καταστάσεων: Loading, Success, Error. Το LaunchedEffect τυλίγεται σε try-catch, και σε επιτυχία ορίζεται state = Success(data), σε σφάλμα — state = Error(exception). Το UI αντιδρά στην κατάσταση και εμφανίζει την αντίστοιχη οθόνη: φορτωτή shimmer, δεδομένα ή οθόνη σφάλματος με κουμπί επανάληψης.

Εάν απαιτείται φόρτωση δεδομένων κατά την κύλιση (pagination), το LaunchedEffect συνδυάζεται με LazyColumn και LazyListState: κατά την επίτευξη του τέλους της λίστας, το κλειδί LaunchedEffect ενημερώνεται (π.χ. μετρητής σελίδων), το οποίο ενεργοποιεί τη φόρτωση της επόμενης παρτίδας δεδομένων.

kotlin
@Composable
fun ArticleScreen(articleId: Int) {
    var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
    
    LaunchedEffect(articleId) {
        state = UiState.Loading
        state = try {
            UiState.Success(articleRepository.fetch(articleId))
        } catch (e: Exception) {
            UiState.Error(e)
        }
    }
    
    when (val s = state) {
        is UiState.Loading -> ShimmerPlaceholder()
        is UiState.Success -> ArticleContent(s.data)
        is UiState.Error -> ErrorScreen(s.error) 
            { // onRetry callback (state updates) }
    }
}

Διαχείριση κλειδιών και επανεκκίνηση

Η σωστή χρήση των κλειδιών LaunchedEffect — το κλειδί για αποτελεσματική εργασία με εφέ (εδώ το λογοπαίγνιο αντικατοπτρίζει την ουσία). Εάν το κλειδί είναι μια μεταβλητή τιμή που αλλάζει συχνά (π.χ. το κείμενο ερωτήματος αναζήτησης σε κάθε εισαγωγή χαρακτήρα), κάθε χαρακτήρας θα ακυρώνει το προηγούμενο coroutine και θα εκκινεί ένα νέο. Για αναζήτηση με καθυστέρηση, αυτό είναι περιττό — είναι καλύτερο να χρησιμοποιήσετε debounce μέσα στο ίδιο το coroutine.

Για την υλοποίηση debounce μέσα στο LaunchedEffect, χρησιμοποιήστε delay() πριν από την εκτέλεση της κύριας ενέργειας. Για παράδειγμα, στην αναζήτηση: το LaunchedEffect(query) εκκινείται σε κάθε αλλαγή του query, αλλά πριν από την εκτέλεση του αιτήματος υπάρχει delay(500). Εάν ο χρήστης εισάγει τον επόμενο χαρακτήρα νωρίτερα από 500 ms, το coroutine ακυρώνεται (λόγω αλλαγής κλειδιού) και εκκινείται ένα νέο — έτσι το αίτημα αποστέλλεται μόνο μετά από παύση 500 ms στην εισαγωγή.

Μια άλλη τεχνική — χρήση sealed class ως κλειδί. Αυτό επιτρέπει τον ακριβή έλεγχο του πότε το εφέ πρέπει να επανεκκινηθεί. Για παράδειγμα, ένα κλειδί-περιτύλιγμα περιέχει το αναγνωριστικό και μια σημαία υποχρεωτικής ενημέρωσης: όταν η σημαία αλλάζει από false σε true, το LaunchedEffect επανεκκινείται ακόμα κι αν το αναγνωριστικό δεν έχει αλλάξει. Ένα τέτοιο μοτίβο είναι βολικό για pull-to-refresh.

kotlin
// Αναζήτηση με debounce 500ms
LaunchedEffect(searchQuery) {
    delay(500)
    searchResults.value = repository.search(searchQuery)
}

// Pull-to-refresh με υποχρεωτική ενημέρωση
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }

LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
    articles = repository.loadUserArticles(userId)
}

Συνήθη λάθη με το LaunchedEffect

Το πρώτο και πιο συνηθισμένο λάθος — χρήση LaunchedEffect χωρίς κλειδιά. Εάν γράψετε LaunchedEffect { ... } χωρίς ορίσματα, το coroutine θα επανεκκινείται σε κάθε ανασύνθεση, οδηγώντας σε έναν ατελείωτο βρόχο αιτημάτων. Το LaunchedEffect απαιτεί τουλάχιστον ένα κλειδί — συνήθως Unit για εφάπαξ εκτέλεση.

Δεύτερο λάθος — προσπάθεια χρήσης LaunchedEffect για εγγραφή σε Flow χωρίς collect. Εάν μέσα στο LaunchedEffect καλέσετε collect σε ένα Flow, το coroutine θα ανασταλεί μέχρι την ολοκλήρωση του Flow (το οποίο στην περίπτωση του StateFlow δεν συμβαίνει ποτέ), και το μπλοκ onDispose δεν θα μπορέσει να ολοκληρωθεί κανονικά. Η σωστή προσέγγιση — χρήση collectLatest, το οποίο ακυρώνει την προηγούμενη συλλογή κατά τη λήψη μιας νέας τιμής.

Τρίτο λάθος — μεταβίβαση ένθετων αντικειμένων ως κλειδί. Εάν το κλειδί είναι μια data class με μεταβλητά πεδία (var), το LaunchedEffect μπορεί να μην αναγνωρίσει την αλλαγή, επειδή το Compose χρησιμοποιεί equals() για σύγκριση, και αυτό για var-πεδία μπορεί να λειτουργήσει απρόβλεπτα. Χρησιμοποιείτε πάντα αμετάβλητα αντικείμενα (val) ή πρωτότυπα ως κλειδιά LaunchedEffect.

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

Τι θα συμβεί εάν δεν μεταβιβαστεί κλειδί στο LaunchedEffect;

Εάν δεν μεταβιβαστούν κλειδιά, το LaunchedEffect δεν θα μεταγλωττιστεί — η Kotlin απαιτεί τουλάχιστον ένα όρισμα για παραμέτρους vararg. Χρησιμοποιήστε LaunchedEffect(Unit) για εφάπαξ εκτέλεση κατά την είσοδο στη σύνθεση ή μεταβιβάστε συγκεκριμένες τιμές στην αλλαγή των οποίων το εφέ πρέπει να επανεκκινηθεί.

Μπορεί το LaunchedEffect να προκαλέσει διαρροή μνήμης;

Όχι, το LaunchedEffect ακυρώνει αυτόματα το coroutine όταν το composable εξέρχεται από τη σύνθεση, αποτρέποντας διαρροές μνήμης. Ωστόσο, εάν το coroutine μέσα στο LaunchedEffect κρατάει αναφορά σε Activity ή Context μέσω ενός κλεισίματος, είναι πιθανή διαρροή — χρησιμοποιήστε viewModelScope για μακροχρόνιες λειτουργίες στο ViewModel.

Ποια είναι η διαφορά μεταξύ LaunchedEffect και rememberCoroutineScope;

Το LaunchedEffect εκτελεί το coroutine αυτόματα κατά την είσοδο στη σύνθεση με σύνδεση σε κλειδιά. Το rememberCoroutineScope παρέχει ένα scope για μη αυτόματη εκκίνηση coroutine, για παράδειγμα, ως απάντηση σε onItemClick. Χρησιμοποιήστε LaunchedEffect για αυτόματα side effects και rememberCoroutineScope για εκκίνηση coroutine βάσει συμβάντων χρήστη.

Γιατί το LaunchedEffect εκτελείται πολλές φορές κατά την ανασύνθεση;

Εάν το κλειδί του LaunchedEffect είναι ασταθής τύπος (π.χ. var ή κλάση χωρίς equals()), το Compose μπορεί να μην αναγνωρίσει ότι η τιμή δεν έχει αλλάξει και θα επανεκκινεί το εφέ σε κάθε ανασύνθεση. Λύση: χρησιμοποιήστε σταθερούς τύπους (πρωτότυπα, συμβολοσειρές, data class με πεδία val) ή τυλίξτε μεταβλητές τιμές σε remember.

Πώς να σταματήσετε το LaunchedEffect χειροκίνητα;

Δεν υπάρχει άμεσος τρόπος να σταματήσετε το LaunchedEffect από έξω — ο έλεγχος γίνεται μέσω κλειδιών. Αλλάξτε το κλειδί για να ακυρώσετε το τρέχον coroutine. Εάν χρειάζεστε πλήρη έλεγχο του κύκλου ζωής του coroutine, χρησιμοποιήστε rememberCoroutineScope με Job και καλέστε μη αυτόματα το job.cancel() σε συμβάν ή αλλαγή κατάστασης.

Περίληψη

  • LaunchedEffect — Jetpack Compose side-effect API για εκκίνηση ασύγχρονων coroutine συνδεδεμένων με τον κύκλο ζωής composable.
  • Κλειδιά — σύστημα επανεκκίνησης βάσει κλειδιών: η αλλαγή κλειδιού ακυρώνει το τρέχον coroutine και εκκινεί ένα νέο με ενημερωμένες παραμέτρους.
  • Αυτόματη ακύρωση — το coroutine ακυρώνεται αυτόματα όταν το composable εξέρχεται από τη σύνθεση, αποτρέποντας διαρροές μνήμης.
  • Φόρτωση δεδομένων — τυπικό μοτίβο: LaunchedEffect(key) για φόρτωση δεδομένων από το δίκτυο με διαχείριση καταστάσεων Loading, Success, Error.
  • Debounce — υλοποιείται μέσω delay() μέσα στο LaunchedEffect: όταν το κλειδί αλλάζει πριν από τη λήξη της καθυστέρησης, το coroutine ακυρώνεται.
  • Συνδρομή Flow — χρησιμοποιήστε collectLatest αντί collect για σωστή επεξεργασία Flow μέσα στο LaunchedEffect.
  • Σταθερά κλειδιά — χρησιμοποιείτε μόνο αμετάβλητους τύπους (val, πρωτότυπα, data class) ως κλειδιά για προβλέψιμη συμπεριφορά.

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

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

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

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