Deferred Navigation (การนำทางแบบรอผล) เป็นรูปแบบการนำทางแบบหน่วงเวลาที่การเปลี่ยนไปยังหน้าจอถัดไปเกิดขึ้นหลังจากดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์ แทนที่จะเกิดขึ้นทันทีในขณะที่ผู้ใช้ดำเนินการ ตามข้อมูลจาก Android Developers (2024) การนำทางแบบรอผลช่วยหลีกเลี่ยงสภาวะการแข่งขันระหว่างการนำทางและการโหลดข้อมูล และช่วยให้การจัดการการเปลี่ยนจาก Push Notification และ Deeplink ง่ายขึ้น ความแตกต่างหลัก — เส้นทางจะถูกคำนวณหลังจากข้อมูลที่จำเป็นทั้งหมดพร้อมใช้งาน
ประเด็นสำคัญ
Deferred Navigation (การนำทางแบบรอผล) เป็นรูปแบบสถาปัตยกรรมที่การตัดสินใจนำทางถูกหน่วงเวลาจนกว่าข้อมูลที่จำเป็นทั้งหมดจะพร้อมใช้งาน ต่างจากการเปลี่ยนโดยตรงที่ผู้ใช้กดปุ่มและไปถึงหน้าจอใหม่ทันที การนำทางแบบรอผลแยกเหตุการณ์ตัวกระตุ้นและการเปลี่ยนจริงออกจากกัน โดยวางการดำเนินการแบบอะซิงโครนัสไว้ระหว่างทั้งสอง
ในทางสถาปัตยกรรม Deferred Navigation สร้างขึ้นบน การเปลี่ยนสถานะ: การกดปุ่มเริ่มกระบวนการแบบอะซิงโครนัส และการติดตามผลลัพธ์ของมันจะกระตุ้นการนำทาง สิ่งนี้สำคัญโดยเฉพาะในแอปพลิเคชันที่มีสถาปัตยกรรม MVVM หรือ MVI ซึ่ง ViewModel จัดการสถานะ และ View (Activity, Fragment, SwiftUI View) ติดตามการเปลี่ยนแปลงและตอบสนองด้วยการเปลี่ยน วิธีการนี้กำจัดการพึ่งพาโดยตรงระหว่าง UI และตรรกะการนำทาง
ตามข้อมูลจาก Google I/O 2023 การนำทางแบบรอผลได้รับการแนะนำสำหรับทุกสถานการณ์ที่การนำทางขึ้นอยู่กับผลลัพธ์ของคำขอเครือข่าย การตรวจสอบการยืนยันตัวตน การโหลดการกำหนดค่า หรือสิทธิ์ รูปแบบนี้ยังจำเป็นเมื่อจัดการกับ Deeplink ซึ่งแอปพลิเคชันต้องเริ่มทำงานก่อน โหลดหน้าจอราก และหลังจากนั้นจึงนำทางไปยังเส้นทางเป้าหมาย
Deferred Navigation ใช้ในสถานการณ์ที่การนำทางโดยตรงนำไปสู่สถานะหน้าจอที่ไม่ถูกต้องหรือข้อผิดพลาดในการโหลด มาดูสี่กรณีหลักที่จำเป็นต้องใช้การนำทางแบบรอผล
หากผู้ใช้แตะเนื้อหาที่ได้รับการป้องกัน แอปพลิเคชันต้องตรวจสอบโทเค็นการเข้าถึงก่อน การนำทางโดยตรง ไปยังหน้าจอเนื้อหาจะส่งผลให้หน้าจอว่างเปล่าหรือข้อผิดพลาด 401 หากโทเค็นหมดอายุ การนำทางแบบรอผลตรวจสอบโทเค็น และเมื่อสำเร็จเท่านั้น — จึงนำทางไปยังหน้าจอเป้าหมาย เมื่อล้มเหลว — เปลี่ยนเส้นทางไปยังหน้าจอเข้าสู่ระบบ
เมื่อแอปพลิเคชันถูกเปิดผ่าน ลิงก์ภายนอก ต้องโหลดหน้าจอรากก่อน กู้คืนสถานะการนำทาง และหลังจากนั้นจึงดำเนินการเปลี่ยน Deeplink การนำทางโดยตรงไปยังหน้าจอเป้าหมายโดยไม่มีบริบทรากจะนำไปสู่ความผิดปกติ: สแต็กการนำทางว่างเปล่าหรือสแต็กย้อนกลับเสียหาย
เมื่อแตะ การแจ้งเตือน Push แอปพลิเคชันสามารถอยู่ในหนึ่งในสามสถานะ: ปิด, อยู่ในพื้นหลัง หรือทำงานอยู่ Deferred Navigation กำหนดสถานะของแอปพลิเคชัน โหลดเนื้อหาที่จำเป็น และหลังจากนั้นจึงแสดงหน้าจอเป้าหมาย iOS อนุญาตให้จัดการสถานการณ์นี้ผ่าน UNNotificationContentExtension
หากฟังก์ชันการทำงานของหน้าจอถูกควบคุมโดย ธงคุณลักษณะ จากเซิร์ฟเวอร์ การนำทางแบบรอผลช่วยให้สามารถขอการกำหนดค่าก่อน และหลังจากนั้นจึงแสดงหน้าจอ หากคุณลักษณะถูกปิดใช้งาน — ผู้ใช้เห็นเนื้อหาทางเลือกหรือตัวยึดแทนที่จะเป็นหน้าจอว่างเปล่า
| สถานการณ์ | การนำทางโดยตรง | Deferred Navigation |
|---|---|---|
| การอนุญาต | หน้าจอว่างเมื่อโทเค็นหมดอายุ | เปลี่ยนเส้นทางไปที่เข้าสู่ระบบ |
| Deeplink | สแต็กย้อนกลับเสียหาย | สแต็กการนำทางที่ถูกต้อง |
| Push | การโหลดโดยไม่มีบริบท | ข้อมูลพร้อมก่อนการเปลี่ยน |
| ธงคุณลักษณะ | แสดงฟังก์ชันที่ไม่พร้อมใช้งาน | ตัวยึดหรือทางเลือก |
การนำทางโดยตรง เป็นวิธีการแบบดั้งเดิมที่การเปลี่ยนถูกดำเนินการทันทีเพื่อตอบสนองต่อเหตุการณ์ ผู้ใช้กดปุ่ม และเราเตอร์ UI เปลี่ยนหน้าจอทันที วิธีการนี้ง่ายและคาดเดาได้ แต่มีข้อจำกัดในสถานการณ์ที่ต้องการข้อมูลจากเซิร์ฟเวอร์หรือการตรวจสอบเงื่อนไข
Deferred Navigation เพิ่มชั้นในรูปแบบของสถานะแบบอะซิงโครนัส เหตุการณ์ผู้ใช้เริ่มดำเนินการ และการติดตามผลลัพธ์ควบคุมการนำทาง สิ่งนี้เพิ่มความซับซ้อนของโค้ด แต่ให้ความยืดหยุ่น: ตัวกระตุ้นเดียวกันสามารถนำไปยังหน้าจอที่แตกต่างกันขึ้นอยู่กับข้อมูลที่โหลด
การเลือกระหว่าง สองวิธี ขึ้นอยู่กับข้อกำหนด: หากการแสดงหน้าจอไม่ต้องการข้อมูลแบบอะซิงโครนัส — ใช้การนำทางโดยตรง หากหน้าจอขึ้นอยู่กับผลลัพธ์ของคำขอ การอนุญาต หรือเงื่อนไขภายนอก — การนำทางแบบรอผลจำเป็น วิธีการแบบผสม ซึ่งการเปลี่ยนบางส่วนโดยตรงและบางส่วนหน่วงเวลา เป็นแนวทางปฏิบัติที่พบบ่อยที่สุดในแอปพลิเคชันอุตสาหกรรม
Android Jetpack มีกลไกสำหรับการนำ Deferred Navigation ไปใช้ในระดับสถาปัตยกรรม แนวคิดหลัก — ViewModel จัดการสถานะ ในขณะที่ Activity หรือ Fragment ติดตามการเปลี่ยนแปลงและกระตุ้นการนำทางผ่าน NavController
StateFlow ใน Coroutine ของ Kotlin เป็นเครื่องมือที่เหมาะสำหรับการนำทางแบบรอผล 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 เพื่อป้องกันการนำทางซ้ำเมื่อหมุนหน้าจอ ใช้ wrapper 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 นำเสนอการทำงานพร้อมกันแบบมีโครงสร้าง ซึ่งอนุญาตให้นำการนำทางแบบรอผลไปใช้ผ่าน 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)
}
}
// In Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Deferred Navigation ทำให้การจัดการสถานการณ์แบบอะซิงโครนัสง่ายขึ้น แต่ต้องการวิธีการที่มีวินัยในการจัดการสถานะ มาดูข้อผิดพลาดหลักที่นักพัฒนาทำเมื่อนำการนำทางแบบรอผลไปใช้
ข้อผิดพลาดที่พบบ่อยที่สุด — การพยายามดำเนินการ การนำทางแบบรอผล ก่อนที่หน้าจอรากจะถูกเริ่มต้นอย่างสมบูรณ์และ NavController หรือ Coordinator พร้อมสำหรับการเปลี่ยน ใน Android สิ่งนี้นำไปสู่ IllegalStateException ใน iOS — นำไปสู่สถานะ UI ที่ไม่ได้กำหนด วิธีแก้ปัญหา — ตรวจสอบให้แน่ใจว่าวงจรชีวิตของคอมโพเนนต์อยู่ในสถานะ STARTED หรือ RESUMED ก่อนกระตุ้นการนำทาง
หาก StateFlow หรือ Subject ไม่ล้างเหตุการณ์หลังจากการประมวลผล เมื่อกลับมาที่หน้าจอก่อนหน้า ผู้ใช้อาจถูกเปลี่ยนเส้นทางไปยังหน้าจอเดียวกันโดยอัตโนมัติ ใช้ SharedFlow ที่มี replay=0 ใน Android หรือ CurrentValueSubject ที่มี nil หลังจากการประมวลผลใน iOS เพื่อให้เหตุการณ์การนำทางทำงานเพียงครั้งเดียว
ใช้ คอมโพเนนต์กลางหนึ่งตัว สำหรับการนำทางทั้งหมดในแอปพลิเคชัน เมื่อแต่ละ Activity, Fragment หรือ ViewController มีตัวควบคุมการนำทางของตัวเอง การนำทางแบบรอผลระหว่างส่วนต่าง ๆ ของแอปพลิเคชันจะวุ่นวาย Coordinator เดียวช่วยให้การดีบักและทดสอบสถานการณ์การนำทางง่ายขึ้น
Deferred Navigation ทดสอบยากกว่าการนำทางโดยตรงเนื่องจากการดำเนินการแบบอะซิงโครนัสเพิ่ม ปัจจัยด้านเวลา ใช้ TestDispatcher ใน Android (kotlinx-coroutines-test) และ XCTestExpectation ใน iOS เพื่อจำลองการโหลดข้อมูลและตรวจสอบว่าการนำทางเป็นไปตามเส้นทางที่คาดหวัง จำลองบริการการอนุญาตและ Deeplink สำหรับการทดสอบแต่ละสถานการณ์อย่างแยกส่วน
คำถามที่พบบ่อย
Deferred Navigation เป็นรูปแบบการเปลี่ยนแบบหน่วงเวลาที่สามารถนำไปใช้ในสถานการณ์แบบอะซิงโครนัสใดก็ได้ Deep Link เป็นตัวกระตุ้นหนึ่งสำหรับการนำทางแบบรอผล แต่ไม่ใช่เพียงตัวเดียว การยืนยันตัวตนและธงคุณลักษณะก็ใช้การนำทางแบบรอผลเช่นกัน
ได้ แอปพลิเคชันส่วนใหญ่ใช้วิธีการแบบผสม หน้าจอรายการสินค้า (ที่ไม่มีการพึ่งพาแบบอะซิงโครนัส) สามารถใช้การนำทางโดยตรง ในขณะที่หน้าจอรายละเอียดที่มีการโหลดข้อมูลใช้การนำทางแบบรอผล การแบ่งแยกถูกกำหนดโดยสถาปัตยกรรมของแต่ละหน้าจอเฉพาะ
ใช้ SharedFlow ที่ไม่มี replay ใน Android และ combineLatest ที่ไม่มีบัฟเฟอร์ใน iOS ยกเลิกการติดตามก่อนหน้าเมื่อมีตัวกระตุ้นใหม่ สิ่งนี้รับประกันว่าประมวลผลเฉพาะเหตุการณ์การนำทางล่าสุดเท่านั้น
ได้ ใน Compose การนำทางแบบรอผลนำไปใช้ผ่านการติดตาม StateFlow ของ ViewModel และเรียก NavController.navigate ใน LaunchedEffect Google แนะนำให้ใช้ Navigation Compose ด้วยโมเดลที่ขับเคลื่อนด้วยเหตุการณ์สำหรับสถานการณ์แบบรอผล
หน่วงเวลา การนำทางจนกว่าแอปพลิเคชันจะกลับมาทำงานอีกครั้ง ใน Android ใช้ Lifecycle.State.STARTED เพื่อกรองเหตุการณ์ ใน iOS ตรวจสอบ UIApplication.State ในบล็อก Combine หรือ async/await
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ