Android Compose Navigation 參數傳遞 & Deep Links

Navigation 2.8.0 之後,我們不再需要像拼湊 URL 字串那樣去組合路由參數。取而代之的是使用 Kotlin SerializationType-Safe Navigation。這讓傳遞參數變得像呼叫函式一樣自然且安全。

基礎參數傳遞

定義 Data Class 作為路由,參數即為建構式參數。

@Serializable
data class Profile(val id: Int, val name: String)

// 導航時直接建立物件
navController.navigate(Profile(id = 123, name = "Alice"))

// 接收參數
composable<Profile> { backStackEntry ->
    val profile: Profile = backStackEntry.toRoute()
    ProfileScreen(profile.id, profile.name)
}

可選參數 (Optional Arguments)

如果某些參數不是必須的,我們可以提供預設值 (Default Value) 並將型別設為 Nullable。Navigation Compose 會自動將其視為可選參數。

@Serializable
data class Search(
    val query: String, 
    val filter: String? = null // 可選參數,預設為 null
)

// 只傳必填參數
navController.navigate(Search(query = "Android")) 

// 傳所有參數
navController.navigate(Search(query = "Android", filter = "Recent"))

傳遞複雜物件 (Complex Objects)

雖然官方建議只傳遞 ID,但有時候傳遞一個小的資料物件 (DTO) 非常方便。在 Type-Safe Navigation 中,這變得容易許多。只要該物件也標註 @Serializable 即可。

@Serializable
data class UserConfig(val darkMode: Boolean, val language: String)

@Serializable
data class Settings(val config: UserConfig) // 巢狀物件

// 導航
navController.navigate(Settings(config = UserConfig(true, "zh-TW")))

// 接收
composable<Settings> { backStackEntry ->
    val settings = backStackEntry.toRoute<Settings>()
    // 直接使用 settings.config
}
注意:雖然技術上可行,但請避免透過導航傳遞大型物件(如包含圖片 byte array 或長列表的物件),這仍受限於 Android Binder Transaction 的 1MB 限制。

Deep Links 允許使用者從 App 外部(例如網頁連結、通知或其他 App)直接跳轉到 App 內的特定頁面。在 Navigation Compose 中,我們可以透過 deepLinks 參數輕鬆實現此功能。

在使用 Type-Safe Navigation 時,Deep Link 的定義變得非常直觀。我們不再需要手動解析 URL 字串,而是定義 basePath,Navigation 系統會自動將 URI 中的變數對應到我們的 Data Class 參數。

1. 定義路由 (Route)

假設我們有一個產品頁面 ProductScreen,它接受一個路徑參數 id (必填) 和一個查詢參數 referrer (選填)。

@Serializable
data class Product(
    val id: String,          // 必填 -> 會對應到 Path 參數
    val referrer: String? = null // 選填 (有預設值) -> 會對應到 Query 參數
)

composable 中加入 deepLinks 設定:

composable<Product>(
    deepLinks = listOf(
        navDeepLink<Product>(basePath = "https://www.example.com/product")
    )
) { backStackEntry ->
    val product: Product = backStackEntry.toRoute()
    ProductScreen(id = product.id, referrer = product.referrer)
}

這個設定會自動處理以下兩種 URL 格式:

  • Path 參數: https://www.example.com/product/{id}
    • 例如:https://www.example.com/product/pixel9
    • 解析結果:Product(id = "pixel9", referrer = null)
  • Query 參數: https://www.example.com/product/{id}?referrer={referrer}
    • 例如:https://www.example.com/product/pixel9?referrer=newsletter
    • 解析結果:Product(id = "pixel9", referrer = "newsletter")
規則很簡單:Data Class 中的必要參數會被視為路徑的一部分 (Path Segment),而可選參數 (Nullable 且有預設值) 則會自動映射為 URL 的查詢字串 (Query Parameter)。

AndroidManifest 設定

為了讓 Android 系統知道你的 App 可以處理這些連結,必須在 AndroidManifest.xml 中對應的 <activity> (通常是 MainActivity) 加入 <intent-filter>

<activity ...>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- 定義 Scheme 和 Host -->
        <!-- 這樣可以匹配 https://www.example.com/product/... -->
        <data android:scheme="https" android:host="www.example.com" android:pathPrefix="/product" />
    </intent-filter>
</activity>

你不需要真的架設網頁伺服器來測試,可以直接使用 Android Debug Bridge (adb) 工具發送 Intent:

# 測試基本路徑
adb shell am start -W -a android.intent.action.VIEW -d "https://www.example.com/product/pixel9"

# 測試包含 Query Parameter
adb shell am start -W -a android.intent.action.VIEW -d "https://www.example.com/product/pixel9?referrer=email"

除了網址,Navigation Compose 也支援基於 Action 和 MimeType 的跳轉。例如,讓使用者從其他 App 分享圖片時直接開啟你的編輯頁面。

@Serializable
object ImageEditor

composable<ImageEditor>(
    deepLinks = listOf(
        navDeepLink {
            action = Intent.ACTION_SEND
            mimeType = "image/*"
        }
    )
) {
    // ... 從 Intent 讀取圖片 URI
}

這同樣需要在 Manifest 中宣告對應的 Intent Filter (Action 為 android.intent.action.SEND, MimeType 為 image/*)。

(Legacy) 舊版字串路由

如果你維護的是舊專案,可能會看到這種寫法。這是 Navigation 2.8.0 之前的標準做法,類似 HTTP URL。

路徑定義"profile/{userId}?name={name}"

  • 必填參數:{userId}
  • 可選參數:?name={name}
composable(
    route = "profile/{userId}?name={name}",
    arguments = listOf(
        navArgument("userId") { type = NavType.StringType },
        navArgument("name") { 
            type = NavType.StringType 
            nullable = true
            defaultValue = null
        }
    )
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getString("userId")
    val name = backStackEntry.arguments?.getString("name")
    // ...
}

這種寫法容易出錯且型別不安全,建議儘快遷移到 Type-Safe Navigation。

小結

  • 總是使用 Type-Safe Navigation (@Serializable) 定義路由。
  • 透過 backStackEntry.toRoute<T>() 輕鬆取得型別安全的參數。
  • 可選參數透過 Kotlin 的預設值 (val arg: String? = null) 實現。
  • Deep Link 依然強大,只需定義 basePath 即可自動對應參數。