Το Deferred Navigation είναι ένα μοτίβο αναβληθείσας πλοήγησης κατά το οποίο η μετάβαση στην επόμενη οθόνη γίνεται μετά την ολοκλήρωση μιας ασύγχρονης λειτουργίας, όχι απευθείας τη στιγμή της ενέργειας του χρήστη. Σύμφωνα με το Android Developers (2024), το deferred navigation επιτρέπει την αποφυγή συνθήκης ανταγωνισμού μεταξύ πλοήγησης και φόρτωσης δεδομένων, καθώς και απλοποιεί τον χειρισμό μεταβάσεων από push ειδοποιήσεις και Deeplink. Η βασική διαφορά — η διαδρομή υπολογίζεται αφού όλα τα απαραίτητα δεδομένα είναι διαθέσιμα.
Κύρια σημεία
Deferred Navigation είναι ένα αρχιτεκτονικό μοτίβο κατά το οποίο η απόφαση πλοήγησης αναβάλλεται μέχρι τη στιγμή που όλα τα απαραίτητα δεδομένα γίνουν διαθέσιμα. Σε αντίθεση με την άμεση μετάβαση, όπου ο χρήστης πατάει ένα κουμπί και μπαίνει αμέσως σε μια νέα οθόνη, το deferred navigation διαχωρίζει το γεγονός-ενεργοποιητή από την πραγματική μετάβαση, προσθέτοντας μια ασύγχρονη λειτουργία μεταξύ τους.
Αρχιτεκτονικά, το Deferred Navigation βασίζεται στην αλλαγή κατάστασης: το πάτημα ενός κουμπιού ξεκινά μια ασύγχρονη διαδικασία και η εγγραφή στο αποτέλεσμά της ενεργοποιεί την πλοήγηση. Αυτό είναι ιδιαίτερα σημαντικό σε εφαρμογές με αρχιτεκτονική MVVM ή MVI, όπου το ViewModel διαχειρίζεται την κατάσταση και το View (Activity, Fragment, SwiftUI View) εγγράφεται στις αλλαγές και αντιδρά με μετάβαση. Αυτή η προσέγγιση εξαλείφει την άμεση εξάρτηση μεταξύ UI και λογικής πλοήγησης.
Σύμφωνα με το Google I/O 2023, το deferred navigation συνιστάται για όλα τα σενάρια όπου η πλοήγηση εξαρτάται από το αποτέλεσμα ενός αιτήματος δικτύου, ελέγχου εξουσιοδότησης, φόρτωσης διαμόρφωσης ή αδειών. Το μοτίβο είναι επίσης υποχρεωτικό κατά την επεξεργασία Deeplink, όταν η εφαρμογή πρέπει πρώτα να εκκινηθεί, να φορτώσει την αρχική οθόνη και μόνο στη συνέχεια να μεταβεί στη διαδρομή-στόχο.
Deferred Navigation εφαρμόζεται σε σενάρια όπου η άμεση μετάβαση οδηγεί σε εσφαλμένη κατάσταση οθόνης ή σφάλματα φόρτωσης. Ας εξετάσουμε τέσσερις κύριες περιπτώσεις όπου το deferred navigation είναι υποχρεωτικό.
Εάν ο χρήστης κάνει κλικ σε προστατευόμενο περιεχόμενο, η εφαρμογή πρέπει πρώτα να ελέγξει το διακριτικό πρόσβασης. Η άμεση πλοήγηση στην οθόνη περιεχομένου θα οδηγήσει σε κενή οθόνη ή σφάλμα 401 εάν το διακριτικό έχει λήξει. Το Deferred navigation ελέγχει το διακριτικό και μόνο σε περίπτωση επιτυχίας μεταβαίνει στην οθόνη-στόχο. Σε περίπτωση αποτυχίας ανακατευθύνει στην οθόνη σύνδεσης.
Όταν η εφαρμογή ανοίγει μέσω ενός εξωτερικού συνδέσμου, πρώτα πρέπει να φορτωθεί η αρχική οθόνη, να αποκατασταθεί η κατάσταση πλοήγησης και μόνο στη συνέχεια να εκτελεστεί η μετάβαση μέσω Deeplink. Η άμεση πλοήγηση στην οθόνη-στόχο χωρίς αρχικό πλαίσιο θα οδηγήσει σε ανωμαλίες: κενή στοίβα πλοήγησης ή κατεστραμμένο back stack.
Κατά το πάτημα μιας push ειδοποίησης, η εφαρμογή μπορεί να βρίσκεται σε τρεις καταστάσεις: κλειστή, σε φόντο ή ενεργή. Το Deferred Navigation καθορίζει την κατάσταση της εφαρμογής, φορτώνει το απαραίτητο περιεχόμενο και μόνο στη συνέχεια εμφανίζει την οθόνη-στόχο. Το iOS επιτρέπει την επεξεργασία αυτού του σεναρίου μέσω UNNotificationContentExtension.
Εάν η λειτουργικότητα της οθόνης ελέγχεται από ένα feature flag από τον διακομιστή, το deferred navigation επιτρέπει πρώτα να ζητηθεί η διαμόρφωση και μόνο στη συνέχεια να εμφανιστεί η οθόνη. Εάν η λειτουργία είναι απενεργοποιημένη, ο χρήστης βλέπει εναλλακτικό περιεχόμενο ή placeholder αντί για κενή οθόνη.
| Σενάριο | Άμεση πλοήγηση | Deferred Navigation |
|---|---|---|
| Εξουσιοδότηση | Κενή οθόνη σε ληγμένο διακριτικό | Ανακατεύθυνση στη σύνδεση |
| Deeplink | Κατεστραμμένο back stack | Σωστή στοίβα πλοήγησης |
| Push | Φόρτωση χωρίς πλαίσιο | Δεδομένα έτοιμα πριν τη μετάβαση |
| Feature flag | Εμφάνιση μη διαθέσιμης λειτουργίας | Placeholder ή εναλλακτική |
Η άμεση πλοήγηση (direct navigation) είναι μια παραδοσιακή προσέγγιση κατά την οποία η μετάβαση εκτελείται αμέσως ως απάντηση σε ένα γεγονός. Ο χρήστης πατάει ένα κουμπί και ο δρομολογητής UI αλλάζει αμέσως οθόνη. Αυτή η προσέγγιση είναι απλή και προβλέψιμη, αλλά περιορισμένη σε σενάρια όπου απαιτούνται δεδομένα από τον διακομιστή ή έλεγχος συνθηκών.
Το Deferred Navigation προσθέτει ένα επίπεδο με τη μορφή ασύγχρονης κατάστασης. Το γεγονός του χρήστη ξεκινά μια λειτουργία και η εγγραφή στο αποτέλεσμα διαχειρίζεται την πλοήγηση. Αυτό αυξάνει την πολυπλοκότητα του κώδικα, αλλά παρέχει ευελιξία: το ίδιο έναυσμα μπορεί να οδηγήσει σε διαφορετικές οθόνες ανάλογα με τα φορτωμένα δεδομένα.
Η επιλογή μεταξύ δύο προσεγγίσεων εξαρτάται από τις απαιτήσεις: εάν για την εμφάνιση της οθόνης δεν απαιτούνται ασύγχρονα δεδομένα — χρησιμοποιήστε άμεση πλοήγηση. Εάν η οθόνη εξαρτάται από το αποτέλεσμα ενός αιτήματος, εξουσιοδότησης ή εξωτερικών συνθηκών — το deferred navigation είναι υποχρεωτικό. Η υβριδική προσέγγιση, όπου μέρος των μεταβάσεων είναι άμεσο και μέρος αναβληθέν, είναι η πιο διαδεδομένη πρακτική σε βιομηχανικές εφαρμογές.
Το Android Jetpack παρέχει μηχανισμούς για την υλοποίηση του Deferred Navigation σε αρχιτεκτονικό επίπεδο. Η βασική ιδέα — το ViewModel διαχειρίζεται την κατάσταση και το Activity ή Fragment εγγράφεται στις αλλαγές και ενεργοποιεί την πλοήγηση μέσω NavController.
Το StateFlow στα Kotlin coroutines είναι το ιδανικό εργαλείο για deferred navigation. Το ViewModel ενημερώνει το StateFlow με το συμβάν πλοήγησης και το Activity το παρατηρεί και εκτελεί τη μετάβαση. Όταν το συμβάν υποβληθεί σε επεξεργασία, το StateFlow καθαρίζεται, αποτρέποντας την επαναλαμβανόμενη πλοήγηση.
class MainViewModel : ViewModel() {
private val _navigation = MutableSharedFlow<NavigationEvent>()
val navigation: SharedFlow<NavigationEvent> = _navigation
fun onDeepLinkReceived(link: String) {
viewModelScope.launch {
val data = resolveDeepLink(link)
_navigation.emit(NavigationEvent.GoToScreen(data))
}
}
}
Στο Activity, η εγγραφή στην πλοήγηση ενεργοποιεί το NavController με τη διαδρομή από το ViewModel. Για την αποτροπή επαναλαμβανόμενης πλοήγησης κατά την περιστροφή οθόνης, χρησιμοποιείται το NavigationEventWrapper που επεξεργάζεται το συμβάν μόνο μία φορά. Το Jetpack Navigation 2.7+ υποστηρίζει Safe Args για ασφαλή μετάδοση ορισμάτων ως προς τον τύπο.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: MainViewModel by viewModels()
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.navigation.collect { event ->
when (event) {
is NavigationEvent.GoToScreen ->
findNavController(R.id.nav_host)
.navigate(event.route)
}
}
}
}
}
Το iOS δεν διαθέτει ενσωματωμένο Navigation Component ανάλογο του Android Jetpack, επομένως οι προγραμματιστές υλοποιούν το Deferred Navigation μέσω Coordinator Pattern σε συνδυασμό με Combine ή async/await. Ο Coordinator διαχειρίζεται τη στοίβα οθονών και λαμβάνει αποφάσεις πλοήγησης βάσει των φορτωμένων δεδομένων.
Ο Coordinator είναι ένα αντικείμενο που διαχειρίζεται την πλοήγηση μεταξύ ViewController. Σε συνδυασμό με το Combine, το ViewModel δημοσιεύει συμβάντα μέσω PassthroughSubject και ο Coordinator εγγράφεται σε αυτά και εκτελεί τη μετάβαση. Αυτή η προσέγγιση διαχωρίζει πλήρως το UI από τη λογική πλοήγησης και συμμορφώνεται με τις συστάσεις της Apple για αρχιτεκτονική εφαρμογών.
final class AppCoordinator {
private var cancellables = Set<AnyCancellable>()
func start(viewModel: MainViewModel) {
viewModel.$navigationDestination
.compactMap { $0 }
.sink { [weak self] destination in
self?.navigateTo(destination)
}
.store(in: &cancellables)
}
}
Το Swift 5.5 εισήγαγε το structured concurrency, το οποίο επιτρέπει την υλοποίηση αναβληθείσας πλοήγησης μέσω async/await χωρίς Combine. Το ViewModel παρέχει μια async συνάρτηση που επιστρέφει τη διαδρομή μετά τη φόρτωση δεδομένων. Ο Coordinator καλεί αυτή τη συνάρτηση σε ένα Task και εκτελεί τη μετάβαση σύμφωνα με τη λαμβανόμενη διαδρομή.
class AuthViewModel: ObservableObject {
func resolveDeeplink(_ url: URL) async -> AppRoute? {
guard let token = await AuthService.shared.getValidToken() else { return .login }
return await DeeplinkRouter.resolve(url, token: token)
}
}
// Στον Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Το Deferred Navigation απλοποιεί τον χειρισμό ασύγχρονων σεναρίων, αλλά απαιτεί πειθαρχημένη προσέγγιση στη διαχείριση κατάστασης. Ας εξετάσουμε τα κύρια λάθη που κάνουν οι προγραμματιστές κατά την υλοποίηση του deferred navigation.
Το πιο συνηθισμένο λάθος — προσπάθεια εκτέλεσης deferred navigation πριν η αρχική οθόνη αρχικοποιηθεί πλήρως και ο NavController ή Coordinator είναι έτοιμος για μετάβαση. Στο Android αυτό οδηγεί σε IllegalStateException, στο iOS — σε απροσδιόριστη κατάσταση UI. Λύση — βεβαιωθείτε ότι ο κύκλος ζωής του στοιχείου βρίσκεται σε κατάσταση STARTED ή RESUMED πριν από την έναρξη της πλοήγησης.
Εάν το StateFlow ή Subject δεν καθαρίζει το συμβάν μετά την επεξεργασία, κατά την επιστροφή στην προηγούμενη οθόνη ο χρήστης μπορεί να ανακατευθυνθεί αυτόματα ξανά στην ίδια οθόνη. Χρησιμοποιήστε SharedFlow με replay=0 στο Android ή CurrentValueSubject με nil μετά την επεξεργασία στο iOS, ώστε το συμβάν πλοήγησης να ενεργοποιείται μόνο μία φορά.
Χρησιμοποιήστε ένα κεντρικό στοιχείο για ολόκληρη την πλοήγηση στην εφαρμογή. Όταν κάθε Activity, Fragment ή ViewController έχει τον δικό του ελεγκτή πλοήγησης, το deferred navigation μεταξύ διαφορετικών τμημάτων της εφαρμογής γίνεται χαοτικό. Ένας ενιαίος Coordinator διευκολύνει τον εντοπισμό σφαλμάτων και τη δοκιμή σεναρίων πλοήγησης.
Το Deferred Navigation είναι πιο δύσκολο να δοκιμαστεί από την άμεση πλοήγηση, καθώς οι ασύγχρονες λειτουργίες εισάγουν παράγοντα χρόνου. Χρησιμοποιήστε TestDispatcher στο Android (kotlinx-coroutines-test) και XCTestExpectation στο iOS για προσομοίωση φόρτωσης δεδομένων και ελέγξτε ότι η πλοήγηση εκτελείται σύμφωνα με την αναμενόμενη διαδρομή. Κάντε mock τις υπηρεσίες εξουσιοδότησης και deeplink για απομονωμένη δοκιμή κάθε σεναρίου.
Συχνές ερωτήσεις
Deferred Navigation είναι ένα μοτίβο αναβληθείσας μετάβασης που μπορεί να εφαρμοστεί σε οποιοδήποτε ασύγχρονο σενάριο. Το Deep Link είναι ένα από τα ερεθίσματα για το deferred navigation, αλλά όχι το μοναδικό. Η εξουσιοδότηση και τα feature flags χρησιμοποιούν επίσης αναβληθείσα πλοήγηση.
Ναι, οι περισσότερες εφαρμογές χρησιμοποιούν υβριδική προσέγγιση. Η οθόνη λίστας προϊόντων (χωρίς ασύγχρονες εξαρτήσεις) μπορεί να χρησιμοποιεί άμεση πλοήγηση, ενώ η οθόνη λεπτομερειών με φόρτωση δεδομένων — deferred. Ο διαχωρισμός καθορίζεται από την αρχιτεκτονική της συγκεκριμένης οθόνης.
Χρησιμοποιήστε SharedFlow χωρίς replay στο Android και combineLatest χωρίς προσωρινή αποθήκευση στο iOS. Ακυρώστε προηγούμενες εγγραφές σε νέο έναυσμα. Αυτό εγγυάται ότι μόνο το τελευταίο συμβάν πλοήγησης υποβάλλεται σε επεξεργασία.
Ναι, στο Compose το deferred navigation υλοποιείται μέσω εγγραφής στο StateFlow του ViewModel και κλήσης του NavController.navigate σε LaunchedEffect. Η Google συνιστά τη χρήση του Navigation Compose με μοντέλο συμβάντων για deferred σενάρια.
Αναβάλετε την πλοήγηση μέχρι η εφαρμογή να γίνει ξανά ενεργή. Στο Android χρησιμοποιήστε το Lifecycle.State.STARTED για φιλτράρισμα συμβάντων. Στο iOS ελέγξτε το UIApplication.State στο μπλοκ Combine ή async/await.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης