1.如何实现Compose中单Activity + 多Page模式,并使用Navigation实现Page与Page间的跳转(携带参数)?
2.如何解决Navigation 在Compose中的拼接式路由配置与拼接式传值?
让我们带着疑问,看下文。
第一步:导入依赖
implementation("androidx.navigation:navigation-compose:2.4.1")
第二步:上代码
#MainActivity.kt
package com.xcy.mynavigationdemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.xcy.mynavigationdemo.page.*
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavContent()
}
}
}
@Composable
fun NavContent() {
val navHostController = rememberNavController()
//startDestination:指定默认进入的页面
NavHost(navController = navHostController, startDestination = "MAIN_PAGE") {
/**
* 使用composable配置页面(举个不恰当的例子,可以先理解成在AndroidManifests.xml中注册Activity)
* 在Compose中使用Navigation,我们可以更容易的实现 单Activity的模式
* 一个Activity + 多个可组合的页面(Composable)
* composable:
* @param route:顾明思议是路由,跳转时需要用到
* @param arguments:跳转时接收的参数,目前比较恶心的就是需要指定key值,并在跳转的时候采用拼接的方式。缺点:配置的地方修改参数名或传参的地方少传或没传时,直接报错
* @param deepLinks: 官方解释:深层链接,Navigation Compose 支持隐式深层链接,此类链接也可定义为 composable() 函数的一部分。使用 navDeepLink() 以列表的形式添加深层链接:
* 具体用法:
* val uri = "https://www.example.com"
* composable("profile?id={id}",deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" }))
* {
* backStackEntry ->Profile(navController, backStackEntry.arguments?.getString("id"))
* }
* 借助这些深层链接,您可以将特定的网址、操作和/或 MIME 类型与可组合项关联起来。默认情况下,这些深层链接不会向外部应用公开。如需向外部提供这些深层链接,您必须向应用的 manifest.xml 文件添加相应的 <intent-filter> 元素。如需启用上述深层链接,您应该在清单的 <activity> 元素中添加以下内容:
* <activity …>
* <intent-filter>
* ...
* <data android:scheme="https" android:host="www.example.com" />
* </intent-filter></activity>
* 本文不对deepLinks做过多解释,如果感兴趣的童鞋,可以去了解一下:https://developer.android.google.cn/jetpack/compose/navigation#deeplinks
* @param content: 当前的目标页面
*/
composable("MAIN_PAGE") { MainPage(navHostController) }
composable(
"FIRST_PAGE/{key1}/{key2}",
arguments = listOf(navArgument("key1"){}, navArgument("key2"){})
) {
FirstPage(navHostController,it.arguments?.getString("key1"), it.arguments?.getString("key2"))
}
composable("SECOND_PAGE?key1={userId}/{key2}",
arguments = listOf(navArgument("key1"){defaultValue = "key1 default value"})
) {
SecondPage(it.arguments?.getString("key1"))
}
}
}
#MainPage.kt(MainActivity.kt中配置的默认页面)
package com.xcy.mynavigationdemo.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
/**
* author:Xcy
* date:2022/5/7 22
* description:主页面
**/
@Composable
fun MainPage(navHostController : NavHostController) {
Column(modifier = Modifier.padding(10.dp).fillMaxSize()) {
Button(onClick = {
/**
* 这里使用参数拼接感觉很恶心,如果MainActivity.kt composable()中更改route参数顺序,这里直接GG
* 假设:composable("FIRST_PAGE/{key2}/{key1}",) {
* FirstPage(it.arguments?.getString("key1"), it.arguments?.getString("key2"))
* }
* 如果这里跳转方法没有改顺序的话
* 原先 key1 = hello key2 = world
* MainActivity.kt中一更改 key1=world key2=hello,导致参数传递就发生了问题
* 这里肯定有头铁的小伙子站出来了:我改了MainActivity.kt,我肯定会更改跳转时的传参顺序啊
* 懂的都懂,随着项目逐渐庞大,也许你会面临着有多处地方需要跳到同一个页面的,跳转时传递的参数还各不相同,这时你修改MainActivity.kt中的路由配置,你将面临着多处不安全改动
*/
navHostController.navigate("FIRST_PAGE/hello/world")
}, modifier = Modifier.fillMaxWidth()) {
Text("跳转FirstPage(必传参数)")
}
}
}
#FirstPage.kt
package com.xcy.mynavigationdemo.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
/**
* author:Xcy
* date:2022/5/7 22
* description:跳转传参(非必选传参)
**/
@Composable
fun FirstPage(navHostController : NavHostController, key1: String?, key2: String?) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
) {
Text(text = "key1=${key1} key2=${key2}")
Button(onClick = {
//注意这里我们没有传递参数,那么Navigation将会使用在MainActivity.kt 中配置的defaultValue
navHostController.navigate("SECOND_PAGE")
}) {
Text(text = "跳转SecondPage")
}
}
}
#SecondPage.kt
package com.xcy.mynavigationdemo.page
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
/**
* author:Xcy
* date:2022/5/7 22
* description:
**/
@Composable
fun SecondPage(key1: String?) {
Text(text = "key1=${key1}")
}
目前为止,你就学会了在Compose中实现单Activity模式 + 多Page,并使用Navigation实现Page与Page间的跳转(携带参数)
通过上面代码,大家估计发现了在Compose中使用Navigation的痛点了,现在我来带大家解决跳转时传参和参数对齐的痛点。
鄙人不才,替大家封装了一个工具类
#NavUtil.kt(简单实现跳转传值)
package com.xcy.mynavigationdemo.util
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.navigation.*
import androidx.navigation.compose.ComposeNavigator
/**
* author:Xcy
* date:2022/5/7 22
* description: 跳转工具类
*
* 使用方法:(声明)
* val navHostController = rememberNavController()
*
* NavUtil.get().init(navHostController = navHostController)
*
* NavHost(navController = navHostController, startDestination = RouteConfig.MAIN_PAGE) {
* composableX(RouteConfig.MAIN_PAGE) { MainPage() }
* composableX(RouteConfig.FIRST_PAGE,
* params = listOf(
* NavParam("key1", isRequired = false, defaultValue = "default value"),
* NavParam("key2"))
* ) {
* FirstPage(it.arguments?.getString("key1"), it.arguments?.getString("key2"))
* }
* composableX(RouteConfig.SECOND_PAGE) { SecondPage() }
* }
*
*默认跳转: NavUtil.get().navigation(RouteConfig.SECOND_PAGE)
*带参跳转: NavUtil.get().navigation(baseRoute, params = hashMapOf<String, String>().apply {
* put("key1", "hello key1")
* put("key2", "hello key2")
* }))
**/
const val NavUtilTAG = "NavUtilTAG"
class NavUtil private constructor() {
private lateinit var navHostController: NavHostController
private var baseRouteInfo = HashMap<String, List<NavParam>>()
companion object {
private val util by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { NavUtil() }
fun get(): NavUtil = util
}
/**
* 初始化
*/
fun init(navHostController: NavHostController) {
this.navHostController = navHostController
}
/**
* 为指定路由绑定需要传递的参数
* @param baseRoute 未拼接或处理的路由
* @param params 当前参数列表
*/
fun bindParam(baseRoute: String, params: List<NavParam>) {
baseRouteInfo[baseRoute] = params
Log.d(NavUtilTAG, "路由:${baseRoute} 参数:${params}")
}
/**
* 获取当前baseRoute下绑定的参数列表
*/
private fun getParam(baseRoute: String): List<NavParam> {
if (baseRouteInfo.containsKey(baseRoute)) {
val params = baseRouteInfo[baseRoute]
if (params?.isNotEmpty()!!) {
return params
}
}
return emptyList()
}
/**
* 跳转
* @param baseRoute 未拼接或处理的路由
*/
fun navigation(baseRoute: String, params: HashMap<String, String> = hashMapOf()) {
val newRoute = StringBuilder()
newRoute.append(baseRoute)
//获取从bindParam()绑定的参数列表,拼接出一个合法的路由(对应composableX()中处理后的路由)
val baseParams = getParam(baseRoute = baseRoute)
if (baseParams.isNotEmpty()) {//当前基础路由绑定的参数列表不为空
newRoute.append("/")
baseParams.forEachIndexed { index, navParam ->
//拼接参数
if (navParam.isRequired) {//(必选参数,如果没有传递则直接报错)
if (params.containsKey(navParam.key)) {
newRoute.append(params[navParam.key])
} else {
Log.d(
NavUtilTAG,
"navigation error(route:${baseRoute} ${navParam.key}为${navParam.isRequired}参数)"
)
}
} else {
if (params.containsKey(navParam.key)) {//当前非必选参数不为空
newRoute.append(params[navParam.key])
} else {//当前参数为非必选,如果没有传递则直接补充为默认值
newRoute.append(navParam.defaultValue)
}
}
if (index < baseParams.size - 1) {
newRoute.append("/")
}
}
} else {//判断当前的参数列表是否存在,或是否全是非必选的字段
val isEmpty = paramsIsEmpty(baseRoute)//是否允许为空
if (!isEmpty) {
printRequiredParam(baseRoute = baseRoute)
}
}
Log.d(NavUtilTAG, "转换后的路由:${newRoute}")
try {
navHostController.navigate(newRoute.toString())
} catch (e: Exception) {
e.printStackTrace()
Log.e(NavUtilTAG, "navigate error:${e.toString()}")
}
}
/**
* 检测当前的路由是否存在参数列表
* 如果存在则判断字段是否全部为非必选
* @param baseRoute 未拼接或处理的路由
* @return 当前参数列表全部为非必选参数或为空时 返回true
*/
private fun paramsIsEmpty(baseRoute: String): Boolean {
if (baseRouteInfo.containsKey(baseRoute)) {//有参数列表
val params = baseRouteInfo[baseRoute]!!
if (params.isNotEmpty()) {//不为空则遍历当前字段是否全部为未拼接
params.forEach {
if (it.isRequired) {
return false
}
}
//当前所有字段全部为非必选
return true
} else {
return false
}
} else {
return true
}
}
/**
* 打印必传参数
*/
private fun printRequiredParam(baseRoute: String) {
val sb = StringBuilder()
if (baseRouteInfo.containsKey(baseRoute)) {//有参数列表
val params = baseRouteInfo[baseRoute]!!
if (params.isNotEmpty()) {//参数列表不为空
params.forEach {
if (it.isRequired) {//当前路由下必传的参数列表
sb.append("${it.key}")
sb.append(" ")
}
}
}
Log.d(NavUtilTAG, "[baseRoute=${baseRoute} 需要传递:${sb}]")
}
}
}
/**
* @param baseRoute 当前需要跳转的路由(未拼接或处理的路由)
* @param params 当前的参数
* @param content 当前跳转的页面
*/
fun NavGraphBuilder.composableX(
baseRoute: String,
params: List<NavParam> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
val newRoute = StringBuilder()
var newParam = mutableListOf<NamedNavArgument>()
newRoute.append(baseRoute)
//将当前基础路由下绑定参数列表
NavUtil.get().bindParam(baseRoute, params = params)
if (params.isNotEmpty()) {//当前参数不为空则拼接路由
newRoute.append("/")
params.forEachIndexed { index, item ->
if (index < params.size) {
//拼接路由
newRoute.append("{${item.key}}")
//拼接参数列表
if (item.isRequired) {
newParam.add(navArgument(item.key) { type = NavType.StringType })
} else {
newParam.add(navArgument(item.key) { item.defaultValue })
}
}
if (index < params.size - 1) {
newRoute.append("/")
}
}
}
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = newRoute.toString()
newParam.forEachIndexed { index, item ->
addArgument(item.name, item.argument)
}
}
)
Log.d(NavUtilTAG, "拼接的路由:${newRoute}")
Log.d(NavUtilTAG, "参数:${params}")
}
/**
* 路由参数
* @param key 参数名
* @param isRequired 当前参数是否为必选参数
* @param defaultValue isRequired为false(非必传参数时) defaultValue必传
*/
data class NavParam(
val key: String,
var isRequired: Boolean = true,
var defaultValue: String = ""
)
#MainActivity.kt(使用NavUtil在MainActivity.kt中配置路由)
package com.xcy.mynavigationdemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.xcy.mynavigationdemo.page.*
import com.xcy.mynavigationdemo.util.NavParam
import com.xcy.mynavigationdemo.util.NavUtil
import com.xcy.mynavigationdemo.util.composableX
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavContent()
}
}
}
@Composable
fun NavContent() {
val navHostController = rememberNavController()
//初始化工具类
NavUtil.get().init(navHostController = navHostController)
NavHost(navController = navHostController, startDestination = RouteConfig.MAIN_PAGE) {
/**
* 简单介绍一下参数
* composableX()
* baseRoute:未拼接的路由,又称基础路由,Navigation该恶心你还是要恶心你的,只是我在工具类中封装了一下,使用工具类拼接参数,拼接参数会导致脏路由,所以这里配置的时候采用基础路由,不需要关心NavUtil中对路由的拼接,获取参数等操作
* params:当前接收的参数列表,我在官方用法上封装了一层,让我来解释一下当前参数列表中的类属性
* NavParam:
* key:当前需要接收的参数名称
* isRequired:当前参数是否为必传参数(默认为true) 如果设置成false的话,在NavUtil中处理时,如果没有获取到传递的参数,将会使用NavParam的defaultValue拼接
* defaultValue:只有在isRequired为false时(当前参数非必传参数),你可以根据自己的需求看是否给他设置一个默认值
* 借助NavParam的isRequired与defaultValue简单的实现了必传参数与非必传参数,设置默认值
* 在NavUtil.navigation()方法中,我做了参数对齐,params为一个map,你只需要传递对应的key和value就行,不需要注意传递时的顺序,至于如何实现参数对齐,感兴趣的可以了解一下源码
*/
composableX(RouteConfig.MAIN_PAGE) { MainPage() }
composableX(RouteConfig.FIRST_PAGE, params = listOf(NavParam("key1", isRequired = false, defaultValue = "default value"), NavParam("key2"))) { FirstPage(it.arguments?.getString("key1"), it.arguments?.getString("key2")) }
composableX(RouteConfig.SECOND_PAGE, params = listOf(
NavParam("key1", isRequired = false, defaultValue = "hello key1"),
NavParam("key2", isRequired = true, defaultValue = "hello key2")
)) { SecondPage(it.arguments?.getString("key1"), it.arguments?.getString("key2")) }
}
}
object RouteConfig {
const val MAIN_PAGE = "MAIN_PAGE"
const val FIRST_PAGE = "FIRST_PAGE"
const val SECOND_PAGE = "SECOND_PAGE"
}
#MainPage.kt(使用NavUtil跳转,传递必传参数)
package com.xcy.mynavigationdemo.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.xcy.mynavigationdemo.RouteConfig
import com.xcy.mynavigationdemo.util.NavUtil
/**
* author:Xcy
* date:2022/5/7 22
* description:主页面
**/
@Composable
fun MainPage() {
Column(modifier = Modifier.padding(10.dp).fillMaxSize()) {
JumpButton(
baseRoute = RouteConfig.FIRST_PAGE,
params = hashMapOf<String, String>().apply {
//当前参数在MainActivity.kt中配置为必传参数,所以都需要传,不然会报错。当然,传递顺序不需要关注
put("key2", "hello key2")
put("key1", "hello key1")
})
}
}
/**
* 带跳转的按钮,不需要关注这个
*/
@Composable
fun JumpButton(baseRoute: String, params: HashMap<String, String>) {
Button(onClick = {
NavUtil.get().navigation(baseRoute, params = params)
}, modifier = Modifier.fillMaxWidth()) {
Text(baseRoute)
}
}
#FirstPage.kt(跳转时只需要传递必传参数即可,非必传参数不传递,会使用默认值)
package com.xcy.mynavigationdemo.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.xcy.mynavigationdemo.RouteConfig
import com.xcy.mynavigationdemo.util.NavUtil
/**
* author:Xcy
* date:2022/5/7 22
* description:
**/
@Composable
fun FirstPage(key1: String?, key2: String?) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
) {
Text(text = "key1=${key1} key2=${key2}")
Button(onClick = {
NavUtil.get()
.navigation(RouteConfig.SECOND_PAGE, params = hashMapOf<String, String>().apply {
put("key2", "hello kotlin")
})
}) {
Text(text = "跳转SecondPage")
}
}
}
#SecondPage.kt
package com.xcy.mynavigationdemo.page
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
/**
* author:Xcy
* date:2022/5/7 22
* description:
**/
@Composable
fun SecondPage(key1: String?, key2: String?) {
Text(text = "key1=${key1} key2=${key2}")
}
ok,大功告成,如果有问题,欢迎一起讨论
完结撒花~~~
徐某人不谈原理,只助你CV
本文代码可通过以下链接获取:
项目链接