Compose页面跳转 (学习Navigation)

1.如何实现Compose中单Activity + 多Page模式,并使用Navigation实现Page与Page间的跳转(携带参数)?
2.如何解决Navigation 在Compose中的拼接式路由配置与拼接式传值?
Compose页面跳转 (学习Navigation)_第1张图片
让我们带着疑问,看下文。
第一步:导入依赖

    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 文件添加相应的  元素。如需启用上述深层链接,您应该在清单的  元素中添加以下内容:
         *     
         *         
         *             ...
         *             
         *         
         *     本文不对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的痛点了,现在我来带大家解决跳转时传参和参数对齐的痛点。
Compose页面跳转 (学习Navigation)_第2张图片
鄙人不才,替大家封装了一个工具类
#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().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>()

    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) {
        baseRouteInfo[baseRoute] = params
        Log.d(NavUtilTAG, "路由:${baseRoute} 参数:${params}")
    }

    /**
     * 获取当前baseRoute下绑定的参数列表
     */
    private fun getParam(baseRoute: String): List {
        if (baseRouteInfo.containsKey(baseRoute)) {
            val params = baseRouteInfo[baseRoute]
            if (params?.isNotEmpty()!!) {
                return params
            }
        }
        return emptyList()
    }

    /**
     * 跳转
     * @param baseRoute 未拼接或处理的路由
     */
    fun navigation(baseRoute: String, params: HashMap = 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 = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    val newRoute = StringBuilder()
    var newParam = mutableListOf()
    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().apply {
                //当前参数在MainActivity.kt中配置为必传参数,所以都需要传,不然会报错。当然,传递顺序不需要关注
                put("key2", "hello key2")
                put("key1", "hello key1")
            })
    }
}

/**
 * 带跳转的按钮,不需要关注这个
 */
@Composable
fun JumpButton(baseRoute: String, params: HashMap) {
    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().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,大功告成,如果有问题,欢迎一起讨论
Compose页面跳转 (学习Navigation)_第3张图片
完结撒花~~~
徐某人不谈原理,只助你CV

Compose页面跳转 (学习Navigation)_第4张图片

本文代码可通过以下链接获取:
项目链接

你可能感兴趣的:(Jetpack,Compose,android,学习,kotlin)