
掘金遷移地址
在Jetpack Compose中導(dǎo)航可以使用Jetpack中的Navigation數(shù)據(jù)傳輸組件進(jìn)行數(shù)據(jù)傳輸。
先決條件
在app的build.gradle中引入Navigation依賴即可,如下:
dependencies {
//導(dǎo)航依賴庫(kù)
implementation "androidx.navigation:navigation-compose:2.4.2"
//Gson解析,后邊用到
implementation 'com.google.code.gson:gson:2.9.0'
}
備注:上述導(dǎo)航組件是沒有動(dòng)畫的,如果需要增加跳轉(zhuǎn)動(dòng)畫,則需要引入google開發(fā)的帶動(dòng)畫的導(dǎo)航庫(kù),如下:
//帶動(dòng)畫的導(dǎo)航依賴庫(kù)
implementation "com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha"
本文主要是通過navigation-compose導(dǎo)航來實(shí)現(xiàn)跳轉(zhuǎn),至于帶動(dòng)畫的,可以自行查看別的文章,實(shí)現(xiàn)大體一致。
使用Navigation導(dǎo)航用到兩個(gè)比較重要的對(duì)象NavHost和NavController。
- NavHost用來承載頁(yè)面,和管理導(dǎo)航圖
- NavController用來控制如何導(dǎo)航還有參數(shù)回退棧等
在官方給的例子中都是通過傳遞常用數(shù)據(jù)類型來實(shí)現(xiàn)跳轉(zhuǎn)時(shí)的參數(shù)傳遞。
我們先用compose實(shí)現(xiàn)需要路由導(dǎo)航的兩個(gè)界面FirstScreen、SecondScreen,代碼如下
@Composable
fun FirstScreen(navigateTo: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
horizontalAlignment = Alignment.CenterHorizontally, //橫向居中
verticalArrangement = Arrangement.Center, //縱向居中
) {
Text(text = "這是第一個(gè)界面")
Button(onClick = {
navigateTo.invoke()
}) {
Text(text = "跳轉(zhuǎn)到第二頁(yè)")
}
}
}
@Composable
fun SecondScreen(name: String?, age: Int?, navigateTo: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Green),
horizontalAlignment = Alignment.CenterHorizontally, //橫向居中
verticalArrangement = Arrangement.Center, //縱向居中
) {
Text(text = "這是第二個(gè)界面 傳遞的參數(shù)為:姓名:$name; 年齡:${age}歲")
Button(onClick = {
navigateTo.invoke()
}) {
Text(text = "跳轉(zhuǎn)到第三頁(yè)")
}
}
}
@Composable
fun ThirdScreen(carName: String?, navigateTo: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray),
horizontalAlignment = Alignment.CenterHorizontally, //橫向居中
verticalArrangement = Arrangement.Center, //縱向居中
) {
Text(text = "這是第三個(gè)界面 carName: $carName")
Button(onClick = {
navigateTo.invoke()
}) {
Text(text = "回到第一頁(yè)")
}
}
}
上述代碼中我們定義了兩個(gè)界面,界面只包含一個(gè)Text和一個(gè)Button,另外為了避免navController在各個(gè)界面中的傳遞,我們定義了一個(gè)函數(shù)navigateTo,用來回調(diào)跳轉(zhuǎn)操作,同時(shí)統(tǒng)一管理跳轉(zhuǎn)實(shí)現(xiàn),便于管理路由。
為了路由不容易出錯(cuò),我們定義兩個(gè)常量,如下
const val ROUTE_FIRST = "routeFirst"
const val ROUTE_SECOND = "routeSecond"
const val ROUTE_THIRD = "routeThird"
另外還需要一個(gè)實(shí)體類,如下
data class User(val name: String? = null, val age: Int = 0): Serializable
到這里我們的準(zhǔn)備工作就做完了,接下來干貨走起
無參數(shù)跳轉(zhuǎn)
我們通過一個(gè)小例子來感受一下
val navController = rememberNavController() //導(dǎo)航控制器
NavHost(
navController = navController,
startDestination = Route.ROUTE_FIRST, //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First`
builder = {
composable(route = Route.ROUTE_FIRST) { //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
FirstScreen {
navController.navigate(Route.ROUTE_SECOND)
}
}
composable(route = Route.ROUTE_SECOND) {
SecondScreen("name", 0) {
navController.navigate("Third?carName=五菱宏光 Mini")
}
}
}
)
通過
rememberNavController()方法創(chuàng)建一個(gè)navController對(duì)象。創(chuàng)建
NavHost對(duì)象,傳入navController并通過startDestination指定啟動(dòng)頁(yè)通過
composable()方法往NavHost中添加頁(yè)面,構(gòu)造方法中的route就代表該頁(yè)面的路徑,后面的函數(shù)就是具體的頁(yè)面。通過
navController的navigate()實(shí)現(xiàn)最終路由跳轉(zhuǎn)
通過上面的代碼我們就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的跳轉(zhuǎn),但是實(shí)際項(xiàng)目中頁(yè)面之間的跳轉(zhuǎn)免不了傳參。那么Compose是如何傳參的呢?
帶參(基本數(shù)據(jù)類型)跳轉(zhuǎn)
參數(shù)傳遞肯定有發(fā)送端和接受端,在Compose中,navController就是發(fā)送端,通過navController.navigate(路由名+參數(shù)值)發(fā)送,接受端通過NavHost的composable(route=路由名+參數(shù)名, arugments = listOf())的route定義參數(shù)名以及通過arguments定義參數(shù)類型。如下偽代碼
NavHost(
navController = navController,
startDestination = "啟動(dòng)頁(yè)路由名A"
builder = {
composable(route = "啟動(dòng)頁(yè)路由名A") {
FirstScreen {
navController.navigate("路由名B/待傳遞參數(shù)b")
}
}
composable(route = "路由名B/{參數(shù)名b}", arguments = listOf(
navArgument("參數(shù)名b") {
type = NavType.IntType //參數(shù)類型
defaultValue = 18 //默認(rèn)值
nullable = true //是否可空
}
)
) {
val name = it.arguments?.getString("參數(shù)名b") //通過`參數(shù)名b`獲取參數(shù)返回值
}
}
)
完整示例:
@Composable
fun NavSample() {
val navController = rememberNavController() //導(dǎo)航控制器
NavHost(
navController = navController,
startDestination = Route.ROUTE_FIRST, //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First`
builder = {
composable(route = Route.ROUTE_FIRST) { //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
FirstScreen {
navController.navigate("${Route.ROUTE_SECOND}/Kevin/10")
}
}
composable(
route = "${Route.ROUTE_SECOND}/{name}/{age}", arguments = listOf(
navArgument("age") {
type = NavType.IntType //類型
}
)
) {
val name = it.arguments?.getString("name")
val age = it.arguments?.getInt("age")
SecondScreen(name, age) {
}
}
}
)
}
帶參(可選基本數(shù)據(jù)類型參數(shù))跳轉(zhuǎn)
上面?zhèn)鬟f的參數(shù)為必傳參數(shù),Navigation Compose還支持可選參數(shù)。可選參數(shù)和必傳參數(shù)有以下兩點(diǎn)不同:
- 可選參數(shù)必須使用查詢參數(shù)語(yǔ)法
?argName={argName}來添加 - 可選參數(shù)必須具有
defaultValue或nullability = true(將默認(rèn)值設(shè)置為 null)
這意味著,所有可選參數(shù)都必須以及列表的形式顯式添加到 composable 方法中
即使沒有傳遞任何參數(shù),系統(tǒng)也會(huì)使用 Default Value 來作為參數(shù)值傳遞到目的地頁(yè)面
完整示例:
@Composable
fun NavSample() {
val navController = rememberNavController() //導(dǎo)航控制器
NavHost(
navController = navController,
startDestination = Route.ROUTE_FIRST, //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First`
builder = {
composable(route = Route.ROUTE_FIRST) { //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
FirstScreen {
navController.navigate("${Route.ROUTE_SECOND}/Kevin/10")
}
}
composable(
route = "${Route.ROUTE_SECOND}/{name}/{age}", arguments = listOf(
navArgument("age") {
type = NavType.IntType //類型
// defaultValue = 18 //默認(rèn)值
// nullable = true //是否可空
}
)
) {
val name = it.arguments?.getString("name")
val age = it.arguments?.getInt("age")
SecondScreen(name, age) {
navController.navigate("${Route.ROUTE_THIRD}?carName=五菱宏光 Mini")
}
}
composable(
route = "${Route.ROUTE_THIRD}?carName={carName}", arguments = listOf(
navArgument("carName") {
defaultValue = "保時(shí)捷卡宴"
}
)
) {
val carName = it.arguments?.getString("carName")
ThirdScreen(carName) {
navController.popBackStack(
route = Route.ROUTE_FIRST,
inclusive = false //是否包含要跳轉(zhuǎn)的路由頁(yè)
)
}
}
}
)
}
帶參(序列化data class)跳轉(zhuǎn)
上述跳轉(zhuǎn)以及帶基本數(shù)據(jù)類型的跳轉(zhuǎn)其實(shí)Google官網(wǎng)說的比我清楚,更多客參考 使用Compose 進(jìn)行導(dǎo)航,但是我們的目的不止如此,我們最終的目的是如何實(shí)現(xiàn) Compose Navigation 傳遞 data class 實(shí)體類?
方案一 使用NavController自帶的Argument屬性
我們先來看看最終代碼,如下
@Composable
fun NavGraphSample1() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Route.ROUTE_FIRST
) {
composable(Route.ROUTE_FIRST) { navBackStackEntry ->
FirstScreen1 {
val args = listOf(Pair("intentText", User("Kevin", 10)))
navController.navigateAndArgument(
Route.ROUTE_SECOND,
args = args
)
}
}
composable(Route.ROUTE_SECOND) {
val intentText = it.arguments?.get("intentText") as User
SecondScreen1(name = intentText.name, intentText.age) {
}
}
}
}
fun NavController.navigateAndArgument(
route: String,
args: List<Pair<String, Any>>? = null,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
) {
navigate(route = route, navOptions = navOptions, navigatorExtras = navigatorExtras)
if (args == null && args?.isEmpty() == true) {
return
}
val bundle = backQueue.lastOrNull()?.arguments
if (bundle != null) {
bundle.putAll(bundleOf(*args?.toTypedArray()!!))
} else {
println("The last argument of NavBackStackEntry is NULL")
}
}
基本上就是這樣獲取和添加Route對(duì)應(yīng)的NavBackStackEntry。
- ? :當(dāng)調(diào)用 NavController#navigate 時(shí),根據(jù)傳遞的路由找到匹配的 DeepLink
backQueue并添加。 - ? : 獲取操作?添加的 NavBackStackEntry 的 Argument。
- ?:將必要的數(shù)據(jù)添加到 Argument。
代碼說明
NavController 內(nèi)部包含我們添加到 BackStack 的 Entry。我們得到這里包含的 NavBackStackEntry 并使用它。
public open class NavController {
// ...
/**
* Retrieve the current back stack.
*
* @return The current back stack.
* @hide
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public open val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()
// ...
}
backQueue 屬性是 LIBRARY_GROUP,因此將來可能無法從外部訪問它。
讓我們看看在 NavBackStackEntry 中添加數(shù)據(jù)的參數(shù)。arguments 屬性定義為可空且只讀,因此實(shí)際訪問時(shí)有可能為空,因此數(shù)據(jù)傳遞可能會(huì)失敗。
public class NavBackStackEntry private constructor(
// ...
/**
* The arguments used for this entry
* @return The arguments used when this entry was created
*/
public val arguments: Bundle? = null,
)
因此,該方案可能出現(xiàn)參數(shù)傳遞為空的情況。
方案二 共享 ViewModel
共享ViewModel相對(duì)就比較簡(jiǎn)單了,直接看代碼
/**
* ViewModel共享數(shù)據(jù)
*/
@Composable
fun NavGraphSample2() {
val viewModel: Sample2ViewModel = viewModel()
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Route.ROUTE_FIRST) {
composable(route = Route.ROUTE_FIRST) {
FirstScreen1 {
viewModel.user = User("Kevin", 11)
navController.navigate(route = Route.ROUTE_SECOND)
}
}
composable(route = Route.ROUTE_SECOND) { navBackStackEntry ->
println("NavGraphSample2 print: user: ${viewModel.user}")
}
}
}
class Sample2ViewModel: ViewModel() {
var user: User? = User()
}
通過對(duì)ViewModel中的user對(duì)象賦值和取值來達(dá)到效果
注意:上述ViewModel只能實(shí)例化一次,也就是賦值和取值應(yīng)該用同一個(gè)ViewModel,否則數(shù)據(jù)將無法共享。
方案三 自定義 NavType 實(shí)現(xiàn)
我們將通過 Serializable/Parcelable。而且,序列化處理使用Kotlin Serialization。使用下面建模的類實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的形式。
我們定義兩個(gè)實(shí)體類,如下
@kotlinx.serialization.Serializable
data class UserSerializable(val name: String? = null, val age: Int = 0) : Serializable
@Parcelize
data class UserParcelable(val name: String? = null, val age: Int = 0) : Parcelable
然后,創(chuàng)建自定義 NavType,出于封裝的考慮,我們還為 Serializable/Parcelable 類型創(chuàng)建了工廠函數(shù)。下面的函數(shù)在每次調(diào)用時(shí)創(chuàng)建并返回一個(gè)新的 NavType。
inline fun <reified T : Serializable> createSerializableNavType(
isNullableAllowed: Boolean = false
): NavType<T> {
return object : NavType<T>(isNullableAllowed) {
override val name: String
get() = "SupportSerializable"
override fun get(bundle: Bundle, key: String): T? { //從Bundle中檢索 Serializable類型
return bundle.getSerializable(key) as? T
}
override fun put(bundle: Bundle, key: String, value: T) { //作為 Serializable 類型添加到 Bundle
bundle.putSerializable(key, value)
}
override fun parseValue(value: String): T { //定義傳遞給 String 的 Parsing 方法
return Gson().fromJson(value, T::class.java)
}
}
}
inline fun <reified T : Parcelable> createParcelableNavType(isNullableAllowed: Boolean = false): NavType<T> {
return object : NavType<T>(isNullableAllowed) {
override val name: String
get() = "SupportParcelable"
override fun get(bundle: Bundle, key: String): T? { //從Bundle中檢索 Parcelable類型
return bundle.getParcelable(key)
}
override fun parseValue(value: String): T { //定義傳遞給 String 的 Parsing 方法
return Gson().fromJson(value, T::class.java)
}
override fun put(bundle: Bundle, key: String, value: T) { //作為 Parcelable 類型添加到 Bundle
bundle.putParcelable(key, value)
}
}
}
接下來,我們使用前面自定義的 NavType 在 NavGraphBuilder 中定義 Composable。
@Composable
fun NavGraphSample3() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Route.ROUTE_FIRST,
builder = {
composable(Route.ROUTE_FIRST) {
FirstScreen1 {
val jsonSerializable = Gson().toJson(UserSerializable("KevinSerializable", 100))
val jsonParcelable = Gson().toJson(UserParcelable("KevinParcelable", 200))
navController.navigate(Route.ROUTE_SECOND + "?key=${Uri.encode(jsonSerializable)}&key1=${Uri.encode(jsonParcelable)}")
}
}
composable(
Route.ROUTE_SECOND + "?key={test_serializable}&key1={test_parcelable}",
arguments = listOf(
navArgument("test_serializable") {
type = createSerializableNavType<UserSerializable>()
},
navArgument( "test_parcelable") {
type = createParcelableNavType<UserParcelable>()
}
)
) { navBackStackEntry -> //根據(jù)導(dǎo)航規(guī)范定義路由和參數(shù)
val arguments = navBackStackEntry.arguments
val userBean = arguments?.getSerializable("test_serializable") as? UserSerializable
val userParcelableBean = arguments?.getParcelable<UserParcelable>("test_parcelable")
println("NavGraphSample3 serializable print: name: ${userBean?.name}; age: ${userBean?.age}")
println("NavGraphSample3 parcelable print: name: ${userParcelableBean?.name}; age: ${userParcelableBean?.age}")
SecondScreen1(name = userBean?.name, age = userBean?.age) {
}
}
}
)
}
首先將
data class實(shí)體類轉(zhuǎn)換成json,通過navigate(路由名?key={參數(shù)1}&key1={參數(shù)2})執(zhí)行數(shù)據(jù)傳遞其次通過
composable進(jìn)行路由定位,并且通過不同的arguments定義不同的類型最后使用
NavBackStackEntry提供的arguments的getSerializable()或者getParcelable()來獲取實(shí)體數(shù)據(jù)
通過上述做法,我們就可以很方便的實(shí)現(xiàn)攜帶Serializable/Parcelable參數(shù)數(shù)據(jù)跳轉(zhuǎn)。