干货-Jectpack Compose 通过Navigation 传递 Serializable / Parcelable三种实现

880481ecbba9baf5d97b4d02ce42b5b4.jpeg

掘金迁移地址

在Jetpack Compose中导航可以使用Jetpack中的Navigation数据传输组件进行数据传输。

先决条件

在app的build.gradle中引入Navigation依赖即可,如下:

dependencies {
    //导航依赖库 
    implementation "androidx.navigation:navigation-compose:2.4.2"
    
    //Gson解析,后边用到
    implementation 'com.google.code.gson:gson:2.9.0'  
}

备注:上述导航组件是没有动画的,如果需要增加跳转动画,则需要引入google开发的带动画的导航库,如下:

//带动画的导航依赖库
implementation "com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha"

本文主要是通过navigation-compose导航来实现跳转,至于带动画的,可以自行查看别的文章,实现大体一致。

使用Navigation导航用到两个比较重要的对象NavHost和NavController。

  • NavHost用来承载页面,和管理导航图
  • NavController用来控制如何导航还有参数回退栈等

在官方给的例子中都是通过传递常用数据类型来实现跳转时的参数传递。

我们先用compose实现需要路由导航的两个界面FirstScreenSecondScreen,代码如下

@Composable
fun FirstScreen(navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Red),

        horizontalAlignment = Alignment.CenterHorizontally,  //横向居中
        verticalArrangement = Arrangement.Center,  //纵向居中
    ) {
        Text(text = "这是第一个界面")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "跳转到第二页")
        }
    }
}

@Composable
fun SecondScreen(name: String?, age: Int?, navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Green),

        horizontalAlignment = Alignment.CenterHorizontally,  //横向居中
        verticalArrangement = Arrangement.Center,  //纵向居中
    ) {
        Text(text = "这是第二个界面 传递的参数为:姓名:$name; 年龄:${age}岁")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "跳转到第三页")
        }
    }
}


@Composable
fun ThirdScreen(carName: String?, navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Gray),

        horizontalAlignment = Alignment.CenterHorizontally,  //横向居中
        verticalArrangement = Arrangement.Center,  //纵向居中
    ) {
        Text(text = "这是第三个界面 carName: $carName")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "回到第一页")
        }
    }
}

上述代码中我们定义了两个界面,界面只包含一个Text和一个Button,另外为了避免navController在各个界面中的传递,我们定义了一个函数navigateTo,用来回调跳转操作,同时统一管理跳转实现,便于管理路由。

为了路由不容易出错,我们定义两个常量,如下

const val ROUTE_FIRST = "routeFirst"

const val ROUTE_SECOND = "routeSecond"

const val ROUTE_THIRD = "routeThird"

另外还需要一个实体类,如下

data class User(val name: String? = null, val age: Int = 0): Serializable

到这里我们的准备工作就做完了,接下来干货走起

无参数跳转

我们通过一个小例子来感受一下

val navController = rememberNavController()  //导航控制器
NavHost(
    navController = navController,
    startDestination = Route.ROUTE_FIRST, //启始页,该参数和`route`相对应,比如我现在启始页是第一个页面,也就是`First` 
    builder = {
        composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名称,跳转时需要
            FirstScreen {
                navController.navigate(Route.ROUTE_SECOND)
            }
        }

        composable(route = Route.ROUTE_SECOND) {

            SecondScreen("name", 0) {
                navController.navigate("Third?carName=五菱宏光 Mini")
            }
        }
    }
)
  • 通过 rememberNavController() 方法创建一个navController对象。

  • 创建 NavHost 对象,传入navController并通过startDestination指定启动页

  • 通过 composable() 方法往NavHost中添加页面,构造方法中的route就代表该页面的路径,后面的函数就是具体的页面。

  • 通过navControllernavigate()实现最终路由跳转

通过上面的代码我们就实现了一个最简单的跳转,但是实际项目中页面之间的跳转免不了传参。那么Compose是如何传参的呢?

带参(基本数据类型)跳转

参数传递肯定有发送端和接受端,在Compose中,navController就是发送端,通过navController.navigate(路由名+参数值)发送,接受端通过NavHostcomposable(route=路由名+参数名, arugments = listOf())的route定义参数名以及通过arguments定义参数类型。如下伪代码

NavHost(
    navController = navController,
    startDestination = "启动页路由名A"
    builder = {
        composable(route = "启动页路由名A") {
            FirstScreen {
                navController.navigate("路由名B/待传递参数b")
            }
        }

        composable(route = "路由名B/{参数名b}", arguments = listOf(
                    navArgument("参数名b") {
                        type = NavType.IntType   //参数类型
                        defaultValue = 18        //默认值
                        nullable = true          //是否可空
                    }
                )
        ) {
        
            val name = it.arguments?.getString("参数名b")  //通过`参数名b`获取参数返回值
        }
    }
)

完整示例:

@Composable
fun NavSample() {
    val navController = rememberNavController()  //导航控制器
    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST,  //启始页,该参数和`route`相对应,比如我现在启始页是第一个页面,也就是`First`
        builder = {
            composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名称,跳转时需要
                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) {
            
                }
            }
        }
    )
}

带参(可选基本数据类型参数)跳转

上面传递的参数为必传参数,Navigation Compose还支持可选参数。可选参数和必传参数有以下两点不同:

  • 可选参数必须使用查询参数语法?argName={argName} 来添加
  • 可选参数必须具有 defaultValuenullability = true (将默认值设置为 null)

这意味着,所有可选参数都必须以及列表的形式显式添加到 composable 方法中

即使没有传递任何参数,系统也会使用 Default Value 来作为参数值传递到目的地页面

完整示例:

@Composable
fun NavSample() {
    val navController = rememberNavController()  //导航控制器
    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST,  //启始页,该参数和`route`相对应,比如我现在启始页是第一个页面,也就是`First`
        builder = {
            composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名称,跳转时需要
                FirstScreen {
                    navController.navigate("${Route.ROUTE_SECOND}/Kevin/10")
                }
            }

            composable(
                    route = "${Route.ROUTE_SECOND}/{name}/{age}", arguments = listOf(
                    navArgument("age") {
                        type = NavType.IntType  //类型
//                    defaultValue = 18  //默认值
//                    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 = "保时捷卡宴"
                    }
                )
            ) {
                val carName = it.arguments?.getString("carName")
                ThirdScreen(carName) {
                    navController.popBackStack(
                        route = Route.ROUTE_FIRST,
                        inclusive = false //是否包含要跳转的路由页
                    )
                }
            }
        }
    )
}

带参(序列化data class)跳转

上述跳转以及带基本数据类型的跳转其实Google官网说的比我清楚,更多客参考 使用Compose 进行导航,但是我们的目的不止如此,我们最终的目的是如何实现 Compose Navigation 传递 data class 实体类

方案一 使用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对应的NavBackStackEntry。

  • ⓵ :当调用 NavController#navigate 时,根据传递的路由找到匹配的 DeepLinkbackQueue并添加。
  • ⓶ : 获取操作⓵添加的 NavBackStackEntry 的 Argument。
  • ⓷:将必要的数据添加到 Argument。

代码说明

NavController 内部包含我们添加到 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 中添加数据的参数。arguments 属性定义为可空且只读,因此实际访问时有可能为空,因此数据传递可能会失败。

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,
)

因此,该方案可能出现参数传递为空的情况。

方案二 共享 ViewModel

共享ViewModel相对就比较简单了,直接看代码

/**
 * ViewModel共享数据
 */
@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()
}

通过对ViewModel中的user对象赋值和取值来达到效果

注意:上述ViewModel只能实例化一次,也就是赋值和取值应该用同一个ViewModel,否则数据将无法共享。

方案三 自定义 NavType 实现

我们将通过 Serializable/Parcelable。而且,序列化处理使用Kotlin Serialization。使用下面建模的类实现了一个简单的形式。

我们定义两个实体类,如下

@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

然后,创建自定义 NavType,出于封装的考虑,我们还为 Serializable/Parcelable 类型创建了工厂函数。下面的函数在每次调用时创建并返回一个新的 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)
        }

    }
}

接下来,我们使用前面自定义的 NavTypeNavGraphBuilder 中定义 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 ->  //根据导航规范定义路由和参数

                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实体类转换成json,通过navigate(路由名?key={参数1}&key1={参数2})执行数据传递

  • 其次通过composable进行路由定位,并且通过不同的arguments定义不同的类型

  • 最后使用NavBackStackEntry提供的argumentsgetSerializable()或者getParcelable()来获取实体数据

通过上述做法,我们就可以很方便的实现携带Serializable/Parcelable参数数据跳转。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容