Router (เราเตอร์) — รูปแบบทางสถาปัตยกรรมที่รวมศูนย์ตรรกะการนำทางระหว่างหน้าจอของแอปพลิเคชัน Router กำหนดว่าจะนำทางไปที่ไหนและอย่างไรเมื่อเกิดเหตุการณ์ ใน iOS Router ทำนามธรรมของ UINavigationController และ segue ใน Android — FragmentManager และ NavController จาก Jetpack Navigation Router สามารถทำงานเป็นรูปแบบอิสระหรือเป็นส่วนประกอบของ Coordinator (Router-per-Coordinator) และ VIPER (Router-per-module) รายละเอียดเพิ่มเติม — ใน Android Navigation Component Guide
ประเด็นสำคัญ
Router — รูปแบบที่ห่อหุ้มตรรกะการเปลี่ยนระหว่างหน้าจอ แนวคิดหลัก: ViewController (หรือ Presenter/ViewModel) ไม่ได้เรียก navigationController.pushViewController โดยตรง แต่แจ้ง Router ถึงความตั้งใจที่จะนำทาง Router ตัดสินใจว่าจะสร้าง ViewController ใดและจะแสดงอย่างไร Router สามารถเรียบง่าย (ชุดของเมธอดการนำทาง) หรือซับซ้อน (พร้อมการกำหนดเส้นทางโครงร่าง URL, deep links และการแจ้งเตือนแบบ push)
สามแนวทางสำหรับ Router — Router ในฐานะบริการ (Navigator singleton ทั่วโลก), Router ในฐานะส่วนประกอบโมดูล (แนวทาง VIPER), Router ในฐานะโปรโตคอล (นามธรรมสำหรับ Coordinator) Router ทั่วโลก — ความเรียบง่าย (หนึ่งอินสแตนซ์ต่อแอป) — เป็นที่นิยมในโปรเจกต์ขนาดเล็ก Router แบบโมดูล — แต่ละฟีเจอร์มี Router ของตัวเอง — มาตรฐาน VIPER และ Clean Architecture Router แบบโปรโตคอล — ใช้ใน Coordinator: RouterProtocol พร้อมเมธอด push/present/pop/dismiss
| แนวทาง | โครงสร้าง | เมื่อใดควรใช้ |
|---|---|---|
| Navigator ทั่วโลก | Singleton พร้อมเมธอดการนำทาง | โปรเจกต์ขนาดเล็ก, ต้นแบบ |
| Router แบบโมดูล | Router ภายในแต่ละโมดูล | VIPER, Clean Architecture |
| Router แบบโปรโตคอล | โปรโตคอล + การใช้งานสำหรับ Coordinator | MVVM-C, Coordinator pattern |
ประวัติของรูปแบบ — Router ปรากฏในเว็บเฟรมเวิร์ก (URL Router — Rails Routes, Express.js) และถูกปรับใช้สำหรับการพัฒนาโมบายล์ ใน iOS Router ได้รับความนิยมจากการเติบโตของ VIPER (2015-2017) ใน Android Router เป็นส่วนหนึ่งของ Jetpack Navigation (2018) โซลูชันการนำทางมาตรฐานของ Google ปัจจุบัน Router เป็นส่วนสำคัญของสถาปัตยกรรมแอปพลิเคชันโมบายล์แบบโมดูล
iOS Router — โปรโตคอลพร้อมเมธอดสำหรับการนำทางประเภทต่างๆ การใช้งานพื้นฐานห่อหุ้ม UINavigationController Router สามารถจัดการการแสดงผลแบบโมดอล (present/dismiss), push/pop ในสแต็กการนำทาง, showDetail สำหรับ Split View Controller และการแสดงผลแบบเต็มหน้าจอ (iOS 13+) Router ไม่ได้สร้าง ViewControllers — รับอินสแตนซ์ที่พร้อมแล้วและแสดงผล การสร้าง ViewController จัดการโดยโรงงาน (Assembly, ตัวบรรจุ DI) หรือ Coordinator
// RouterProtocol สำหรับ iOS
protocol RouterProtocol {
func push(_ viewController: UIViewController, animated: Bool)
func pop(animated: Bool)
func popToRoot(animated: Bool)
func present(_ viewController: UIViewController, animated: Bool)
func dismiss(animated: Bool)
func setViewControllers(_: [UIViewController], animated: Bool)
}
// การใช้งาน Router ผ่าน UINavigationController
class NavigationRouter: RouterProtocol {
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push(_ vc: UIViewController, animated: Bool) {
if navigationController.presentedViewController {
navigationController.dismiss(animated: false)
}
navigationController.pushViewController(vc, animated: animated)
}
func pop(animated: Bool) {
navigationController.popViewController(animated: animated)
}
func popToRoot(animated: Bool) {
navigationController.popToRootViewController(animated: animated)
}
func present(_ vc: UIViewController, animated: Bool) {
navigationController.present(vc, animated: animated)
}
func dismiss(animated: Bool) {
navigationController.dismiss(animated: animated)
}
func setViewControllers(_ vcs: [UIViewController], animated: Bool) {
navigationController.setViewControllers(vcs, animated: animated)
}
}
// Assembly — การสร้างโมดูลด้วย Router
protocol ProfileModuleFactory {
func makeProfileView() -> UIViewController
}
Router ใน VIPER — แต่ละโมดูล VIPER มี Router ของตัวเอง ซึ่งรู้ว่าหน้าจอใดควรเปิดจากโมดูลปัจจุบัน Router สร้าง ViewController ของโมดูลถัดไปผ่านโรงงานและส่งต่อการควบคุม Router ใน VIPER เป็นส่วนประกอบเดียวที่มี import UIKit (ยกเว้น View) ซึ่งทำให้การทดสอบ Presenter และ Interactor ง่ายขึ้น: ไม่ขึ้นอยู่กับ UIKit
Jetpack Navigation Component — เราเตอร์มาตรฐานสำหรับ Android ที่ Google แนะนำตั้งแต่ปี 2018 การนำทางถูกกำหนดในกราฟ XML (nav_graph.xml): โหนด (fragments, activities, destinations) และขอบ (actions) NavController จัดการการเปลี่ยน: เรียก FragmentManager.replace/add และจัดการสแต็กย้อนกลับ Navigation Component รองรับ deep links, อาร์กิวเมนต์, แอนิเมชันการเปลี่ยน และ Safe Args สำหรับการส่งข้อมูลแบบ type-safe
// nav_graph.xml — กราฟการนำทาง
<!-- @xml/nav_graph.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/loginFragment">
<fragment android:id="@+id/loginFragment"
android:name=".ui.login.LoginFragment">
<action android:id="@+id/to_home"
app:destination="@id/homeFragment" />
</fragment>
<fragment android:id="@+id/homeFragment"
android:name=".ui.home.HomeFragment">
<argument android:name="userId"
android:defaultValue="0"
app:argType="integer" />
<deepLink app:uri="myapp://home/{userId}" />
</fragment>
</navigation>
// Kotlin — การนำทางผ่าน NavController
class LoginFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.loginButton.setOnClickListener {
val action = LoginFragmentDirections.toHome(userId = 42)
findNavController().navigate(action)
}
}
}
// Safe Args — อาร์กิวเมนต์แบบ type-safe
class HomeFragmentArgs : NavArgs {
val userId: Int get() = arguments?.getInt("userId") ?: 0
}
// NavigationUI — การรวมกับ Toolbar, BottomNav, Drawer
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHost.navController
NavigationUI.setupActionBarWithNavController(this, navController)
}
}
Deep links ใน Android Navigation — Navigation Component รองรับ deep links แบบชัดแจ้ง (PendingIntent + NavDeepLinkBuilder) และโดยนัย (ผ่าน intent-filter ในแมนิเฟสต์) deep link สามารถนำไปยังโหนดใดๆ ใน nav_graph การจัดการการแจ้งเตือน push: Notification สร้าง PendingIntent ด้วย NavDeepLinkBuilder ซึ่งกู้คืนสแต็กย้อนกลับและเปิดหน้าจอที่ต้องการ Navigation Component จัดการปุ่ม Up/Back โดยอัตโนมัติและส่งผู้ใช้กลับไปยังหน้าจอก่อนหน้า
URL Routing — การกำหนดเส้นทางตามโครงร่าง URL, deep links และ universal links แอปลงทะเบียนโครงร่าง (myapp://profile/42) หรือ universal link (https://example.com/profile/42) Router แยกวิเคราะห์ URL, ดึงพารามิเตอร์ (userId=42) และเปิดหน้าจอที่เกี่ยวข้อง URL Routing ช่วยให้เปิดแอปจากภายนอก: ผ่านลิงก์จากเว็บไซต์, อีเมล, การแจ้งเตือน push, QR โค้ด iOS ใช้ NSUserActivity + universal link, Android — intent-filter + deep link
// URL Router — การแยกวิเคราะห์ deep link และการนำทาง
protocol URLRoute {
var pattern: String { get }
func navigate(parameters: [String: String], router: RouterProtocol)
}
struct ProfileRoute: URLRoute {
let pattern = "myapp://profile/{userId}"
func navigate(parameters: [String: String], router: RouterProtocol) {
guard let userId = parameters["userId"] else { return }
let profileVC = ProfileViewController(userId: userId)
router.push(profileVC, animated: true)
}
}
// URL Router — การจับคู่ URL กับเส้นทาง
class URLRouter {
private var routes: [URLRoute] = []
func handle(_ url: URL, router: RouterProtocol) {
for route in routes {
if let params = matchPattern(route.pattern, url: url) {
route.navigate(parameters: params, router: router)
return
}
}
}
}
การกำหนดเส้นทาง deep link ในทางปฏิบัติ — แอปต้องจัดการ deep links ในสามสถานะ: ยังไม่เริ่มทำงาน (เริ่มด้วย deep link), ในพื้นหลัง (กู้คืนจากพื้นหลัง), กำลังทำงาน (onNewIntent/SceneDelegate) URL Router ต้องกู้คืนสแต็กย้อนกลับอย่างถูกต้อง: เมื่อ deep link ไปยังหน้าจอโปรไฟล์ ผู้ใช้ควรสามารถกด Back และกลับไปยังหน้าจอก่อนหน้าได้ iOS และ Android จัดการสแต็กย้อนกลับต่างกัน — Router ต้องคำนึงถึงลักษณะเฉพาะของแต่ละแพลตฟอร์ม
Router vs Coordinator — Router ตอบคำถาม "จะแสดงหน้าจออย่างไร?" (push/present), Coordinator — "ควรแสดงหน้าจอใด?" (เริ่มโฟลว์ใด) Router เป็นเครื่องมือนำทาง Coordinator เป็นผู้จัดระเบียบโฟลว์ Coordinator ใช้ Router ใน VIPER Router ทำหน้าที่ทั้งสอง: ตัดสินใจว่าจะไปที่ไหนและจะแสดงอย่างไร ใน MVVM-C Router เป็นโปรโตคอลแยกต่างหากที่ Coordinator ใช้ รูปแบบไม่ได้แยกจากกันแต่เสริมซึ่งกันและกัน
| ลักษณะ | Router | Coordinator | Navigator |
|---|---|---|---|
| จุดเน้น | กลไกการนำทาง | โฟลว์การนำทาง | การเข้าถึงทั่วโลก |
| ระดับ | UI framework (push/present) | ตรรกะทางธุรกิจ (โฟลว์) | โครงสร้างพื้นฐาน |
| การสร้างหน้าจอ | รับ VC ที่พร้อมแล้ว | สร้าง VC ผ่านโรงงาน | สร้าง VC |
| วงจรชีวิต | หนึ่งครั้งต่อการนำทาง | childCoordinators | Singleton |
Router vs Navigator — Navigator เป็นชื่อเก่าของ Router ในชุมชน iOS ในการใช้งานแรกเริ่ม Navigator เป็นคลาส Singleton พร้อมเมธอดการนำทาง Navigator เป็น Router singleton ทั่วโลก Navigator ง่ายกว่าแต่สร้างการพึ่งพาที่ซ่อนอยู่ Router ที่ใช้โปรโตคอลสามารถทดสอบได้และเป็นโมดูลมากกว่า Navigator เหมาะสำหรับโปรเจกต์ที่ไม่มีสถาปัตยกรรมที่เคร่งครัด Router — สำหรับ Clean Architecture และ VIPER
คำถามที่พบบ่อย
Router เป็นนามธรรมเหนือ UINavigationController (push/present/pop) Coordinator เป็นชั้นเหนือ Router ที่จัดการโฟลว์ (หน้าจอใดจะแสดงถัดไป) Router จัดการกลไก Coordinator จัดการตรรกะการนำทาง Coordinator ใช้ Router เพื่อดำเนินการเปลี่ยน ใน VIPER Router รวมทั้งสองหน้าที่ ใน MVVM-C แยกกัน
ไม่ สำหรับแอปที่มี 3-5 หน้าจอ การเรียก navigationController.pushViewController โดยตรงเป็นวิธีที่ยอมรับได้ Router จะมีประโยชน์เมื่อมี 10+ หน้าจอ, deep links, การแจ้งเตือน push และสถาปัตยกรรมแบบโมดูล Router ช่วยให้การทดสอบการนำทางง่ายขึ้นและช่วยให้จัดการการเปลี่ยนจากแหล่งใดๆ (ปุ่ม, deep link, การแจ้งเตือน push) ได้จากส่วนกลาง
URL Router เป็นส่วนขยายของ Router ที่แยกวิเคราะห์ deep links และ universal links URL Router กำหนดการจับคู่ระหว่าง URL และหน้าจอของแอป ตัวอย่างเช่น เส้นทาง myapp://profile/42 → ProfileViewController(userId: 42) URL Router รวมศูนย์การจัดการ deep links จากแหล่งต่างๆ: การแจ้งเตือน push, QR โค้ด, ลิงก์อีเมล, ลิงก์สากล
ได้ Router สามารถทำงานอิสระ — ในฐานะ Navigator ทั่วโลกหรือเป็นส่วนประกอบโมดูล VIPER Router ที่ไม่มี Coordinator เป็นเพียงการห่อหุ้ม UINavigationController ไว้ด้านหลังโปรโตคอล Coordinator ที่ไม่มี Router ซับซ้อนกว่า (ViewController เรียก push/present โดยตรง) แต่เป็นไปได้ แนวทางปฏิบัติที่ดีที่สุดคือใช้ Router + Coordinator ร่วมกัน
ในแง่การทำงาน — ใช่ ทั้งคู่รวมศูนย์การนำทาง การใช้งานต่างกัน: iOS Router — โปรโตคอล + คลาสรอบ UINavigationController; Android — Jetpack Navigation Component พร้อมกราฟ XML และ NavController Android Router ถูกสร้างในเฟรมเวิร์ก (Navigation Component), iOS Router เป็นการใช้งานของบุคคลที่สาม Android Router รองรับ Safe Args, iOS Router ไม่รองรับ (ต้องการ generics หรือ Codable)
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม