A navegação do Compose é difícil de usar? Vou embalar um super fácil de usar para você!

O componente de navegação oferece suporte a aplicativos Jetpack Compose . Podemos navegar entre elementos que podem ser compostos aproveitando a infraestrutura e os recursos do componente de navegação. No entanto, depois de usá-lo no projeto, descobri que esse componente realmente não é fácil de usar:

  • Acoplamento: a navegação precisa conter NavHostController Em funções combináveis, NavHostController deve ser passado para navegar, portanto, todas as funções combináveis ​​que precisam navegar devem conter uma referência a NavHostController . Passar callbacké o mesmo problema.
  • A refatoração e o encapsulamento tornam-se difíceis: alguns projetos não são um projeto Compose completamente novo, mas reescritas funcionais parciais; nesse caso, é difícil fornecer NavHostController a essas composições.
  • A função de salto é problemática. Em muitos casos, não é simplesmente para navegar para a próxima página. Ela pode ser acompanhada por replace, pope limpar a pilha de navegação, que requer muito código para ser implementada.
  • ViewModeletc. Funções não combináveis ​​não podem obter NavHostController .
  • A emenda de nomes de rota é problemática: se a rota do componente de navegação passar por parâmetros, ela precisa ser emendada de acordo com as regras.

Li muitas discussões sobre como implementar a navegação e encontrei algumas ótimas bibliotecas, appyx , compose-router , Decompose , compose-backstack e compose-destinations com a maioria dos usuários , mas nenhuma delas me satisfez, afinal navegação é o mais importante É muito importante, então vou transformar o componente Navigation e encapsular uma biblioteca de componentes conveniente.

Jetpack Compose Navegação Limpa

Se você usar um singleton ou Hiltfornecer um navegador personalizado para um singleton, poderá usá-lo diretamente em cada ViewModelcontexto Composee navegar para telas diferentes chamando as funções do navegador. Todos os eventos de navegação podem ser coletados juntos para que não haja necessidade de passar callbacks ou passar navControllerpara outras telas. Quando você chegar ao uso conciso da frase a seguir, pergunte se cheira bem ou não.

            AppNav.to(ThreeDestination("来自Two"))
            AppNav.replace(ThreeDestination("replace来自Two"))
            AppNav.back()
复制代码

Para implementar um navegador personalizado, primeiro use a interface para declarar as funções necessárias. De um modo geral, as duas primeiras funções de pilha e navegação podem atender às necessidades do aplicativo e as funções das duas últimas funções também podem usar as duas primeiras funções. Ele é implementado, mas há um pouco mais de parâmetros e muitos cenários de uso reais. Para simplificar, use as duas funções a seguir para expandir:

interface INav {


    /**
     * 出栈
     * @param route String
     * @param inclusive Boolean
     */
    fun back(
        route: String? = null,
        inclusive: Boolean = false,
    )

    /**
     * 导航
     * @param route 目的地路由
     * @param popUpToRoute 弹出路由?
     * @param inclusive 是否也弹出popUpToRoute
     * @param isSingleTop Boolean
     */
    fun to(
        route: String,
        popUpToRoute: String? = null,
        inclusive: Boolean = false,
        isSingleTop: Boolean = false,
    )


    /**
     * 弹出当前栈并导航到
     * @param route String
     * @param isSingleTop Boolean
     */
    fun replace(
        route: String,
        isSingleTop: Boolean = false,
    )

    /**
     * 清空导航栈然后导航到route
     * @param route String
     */
    fun offAllTo(
        route: String,
    )


}

复制代码

AppNavAs quatro funções de navegação acima são realizadas. É bem simples, pois você precisa usar um singleton, que é usado aqui object, que apenas adiciona uma função private para enviar as intenções de navegação:

object AppNav : INav {

    private fun navigate(destination: NavIntent) {
        NavChannel.navigate(destination)
    }

    override fun back(route: String?, inclusive: Boolean) {
        navigate(NavIntent.Back(
            route = route,
            inclusive = inclusive,
        ))
    }


    override fun to(
        route: String,
        popUpToRoute: String?,
        inclusive: Boolean,
        isSingleTop: Boolean,
    ) {
        navigate(NavIntent.To(
            route = route,
            popUpToRoute = popUpToRoute,
            inclusive = inclusive,
            isSingleTop = isSingleTop,
        ))
    }

    override fun replace(route: String, isSingleTop: Boolean) {
        navigate(NavIntent.Replace(
            route = route,
            isSingleTop = isSingleTop,
        ))

    }

    override fun offAllTo(route: String) {
        navigate(NavIntent.OffAllTo(route))
    }


}
复制代码

NavIntentÉ a intenção de navegação, que corresponde a cada função do navegador. Assim como o navegador, duas funções são suficientes, e mais duas funções também são para simplificar:

sealed class NavIntent {

    /**
     * 返回堆栈弹出到指定目标
     * @property route 指定目标
     * @property inclusive 是否弹出指定目标
     * @constructor
     * 【"4"、"3"、"2"、"1"】 Back("2",true)->【"4"、"3"】
     * 【"4"、"3"、"2"、"1"】 Back("2",false)->【"4"、"3"、"2"】
     */
    data class Back(
        val route: String? = null,
        val inclusive: Boolean = false,
    ) : NavIntent()


    /**
     * 导航到指定目标
     * @property route 指定目标
     * @property popUpToRoute 返回堆栈弹出到指定目标
     * @property inclusive 是否弹出指定popUpToRoute目标
     * @property isSingleTop 是否是栈中单实例模式
     * @constructor
     */
    data class To(
        val route: String,
        val popUpToRoute: String? = null,
        val inclusive: Boolean = false,
        val isSingleTop: Boolean = false,
    ) : NavIntent()

    /**
     * 替换当前导航/弹出当前导航并导航到指定目的地
     * @property route 当前导航
     * @property isSingleTop 是否是栈中单实例模式
     * @constructor
     */
    data class Replace(
        val route: String,
        val isSingleTop: Boolean = false,
    ) : NavIntent()

    /**
     * 清空导航栈并导航到指定目的地
     * @property route 指定目的地
     * @constructor
     */
    data class OffAllTo(
        val route: String,
    ) : NavIntent()

}

复制代码

Para conseguir ViewMdeolenviar em vários locais (funções combináveis) e receber e processar comandos de navegação em um local, você precisa usar o Flow ou Channelimplementá-lo aqui Channel. Além disso object, se você usá Hilt-lo, poderá fornecer um singleton:

internal object NavChannel {

    private val channel = Channel<NavIntent>(
        capacity = Int.MAX_VALUE,
        onBufferOverflow = BufferOverflow.DROP_LATEST,
    )

    internal var navChannel = channel.receiveAsFlow()

    internal fun navigate(destination: NavIntent) {
        channel.trySend(destination)
    }
}
复制代码

实现接收并执行对应功能:

fun NavController.handleComposeNavigationIntent(intent: NavIntent) {
    when (intent) {
        is NavIntent.Back -> {
            if (intent.route != null) {
                popBackStack(intent.route, intent.inclusive)
            } else {
                currentBackStackEntry?.destination?.route?.let {
                    popBackStack()
                }
            }
        }
        is NavIntent.To -> {
            navigate(intent.route) {
                launchSingleTop = intent.isSingleTop
                intent.popUpToRoute?.let { popUpToRoute ->
                    popUpTo(popUpToRoute) { inclusive = intent.inclusive }
                }
            }
        }
        is NavIntent.Replace -> {
            navigate(intent.route) {
                launchSingleTop = intent.isSingleTop
                currentBackStackEntry?.destination?.route?.let {
                    popBackStack()
                }
            }
        }

        is NavIntent.OffAllTo -> navigate(intent.route) {
            popUpTo(0)
        }
    }
}
复制代码

自定义NavHostcomposable. NavigationEffects只需收集navigationChannel并导航到所需的屏幕。这里可以看到,它很干净干净,我们不必传递任何回调或navController.

@Composable
fun NavigationEffect(
    startDestination: String, builder: NavGraphBuilder.() -> Unit,
) {
    val navController = rememberNavController()
    val activity = (LocalContext.current as? Activity)
    val flow = NavChannel.navChannel
    LaunchedEffect(activity, navController, flow) {
        flow.collect {
            if (activity?.isFinishing == true) {
                return@collect
            }
            navController.handleComposeNavigationIntent(it)
            navController.backQueue.forEachIndexed { index, navBackStackEntry ->
                Log.e(
                    "NavigationEffects",
                    "index:$index=NavigationEffects: ${navBackStackEntry.destination.route}",
                )
            }
        }
    }
    NavHost(
        navController = navController,
        startDestination = startDestination,
        builder = builder
    )
}
复制代码

导航封装完成,还有一步就是路由间的参数拼接,最初的实现是使用者自己实现:

sealed class Screen(
    path: String,
    val arguments: List<NamedNavArgument> = emptyList(),
) {
    val route: String = path.appendArguments(arguments)

    object One : Screen("one")
    object Two : Screen("two")
    object Four : Screen("four", listOf(
        navArgument("user") {
            type = NavUserType()
            nullable = false
        }
    )) {
        const val ARG = "user"
        fun createRoute(user: User): String {
            return route.replace("{${arguments.first().name}}", user.toString())
        }
    }

    object Three : Screen("three",
        listOf(navArgument("channelId") { type = NavType.StringType })) {
        const val ARG = "channelId"
        fun createRoute(str: String): String {
            return route.replace("{${arguments.first().name}}", str)
        }
    }
}
复制代码

优点是使用密封类实现路由声明,具有约束作用。后来考虑到减少客户端样板代码,就声明了一个接口,appendArguments是拼接参数的扩展方法,无需自己手动拼接:

abstract class Destination(
    path: String,
    val arguments: List<NamedNavArgument> = emptyList(),
) {
    val route: String = if (arguments.isEmpty()) path else path.appendArguments(arguments)
}

private fun String.appendArguments(navArguments: List<NamedNavArgument>): String {
    val mandatoryArguments = navArguments.filter { it.argument.defaultValue == null }
        .takeIf { it.isNotEmpty() }
        ?.joinToString(separator = "/", prefix = "/") { "{${it.name}}" }
        .orEmpty()
    val optionalArguments = navArguments.filter { it.argument.defaultValue != null }
        .takeIf { it.isNotEmpty() }
        ?.joinToString(separator = "&", prefix = "?") { "${it.name}={${it.name}}" }
        .orEmpty()
    return "$this$mandatoryArguments$optionalArguments"
}
复制代码

使用

首先声明路由,继承Destination,命名采用page+Destination

object OneDestination : Destination("one")
object TwoDestination : Destination("two")

object ThreeDestination : Destination("three",
    listOf(navArgument("channelId") { type = NavType.StringType })) {
    const val ARG = "channelId"
    operator fun invoke(str: String): String = route.replace("{${arguments.first().name}}", str)
}


object FourDestination : Destination("four", listOf(
    navArgument("user") {
        type = NavUserType()
        nullable = false
    }
)) {
    const val ARG = "user"
    operator fun invoke(user: User): String =
        route.replace("{${arguments.first().name}}", user.toString())
}

object FiveDestination : Destination("five",
    listOf(navArgument("age") { type = NavType.IntType },
        navArgument("name") { type = NavType.StringType })) {
    const val ARG_AGE = "age"
    const val ARG_NAME = "name"
    operator fun invoke(age: Int, name: String): String =
        route.replace("{${arguments.first().name}}", "$age")
            .replace("{${arguments.last().name}}", name)
}
复制代码

传递普通参数,String、Int

使用navArgument生命参数名和类型,然后用传参替换对应的参数名,这里使用invoke简化写法:

object ThreeDestination : Destination("three",
    listOf(navArgument("channelId") { type = NavType.StringType })) {
    const val ARG = "channelId"
    operator fun invoke(str: String): String = route.replace("{${arguments.first().name}}", str)
}
复制代码

传递多个参数

用传参去去替换路由里面对应的参数名。

object FiveDestination : Destination("five",
    listOf(navArgument("age") { type = NavType.IntType },
        navArgument("name") { type = NavType.StringType })) {
    const val ARG_AGE = "age"
    const val ARG_NAME = "name"
    operator fun invoke(age: Int, name: String): String =
        route.replace("{${arguments.first().name}}", "$age")
            .replace("{${arguments.last().name}}", name)
}
复制代码

传递序列化参数

DataBean 要序列化,这里用了两个注解,Serializable是因为使用了kotlinx.serialization,如果使用 Gson 则不需要,重写toString是因为拼接参数的时候可以直接用。

@Parcelize
@kotlinx.serialization.Serializable
data class User(
    val name: String,
    val phone: String,
) : Parcelable{
    override fun toString(): String {
        return Uri.encode(Json.encodeToString(this))
    }
}
复制代码

然后自定义NavType

class NavUserType : NavType<User>(isNullableAllowed = false) {

    override fun get(bundle: Bundle, key: String): User? =
        bundle.getParcelable(key)

    override fun put(bundle: Bundle, key: String, value: User) =
        bundle.putParcelable(key, value)

    override fun parseValue(value: String): User {
        return Json.decodeFromString(value)
    }

    override fun toString(): String {
        return Uri.encode(Json.encodeToString(this))
    }

}
复制代码

传递自定义的NavType

object FourDestination : Destination("four", listOf(
    navArgument("user") {
        type = NavUserType()
        nullable = false
    }
)) {
    const val ARG = "user"
    operator fun invoke(user: User): String =
        route.replace("{${arguments.first().name}}", user.toString())
}
复制代码

注册

使用NavigationEffect替换原生的NavHost

                    NavigationEffect(OneDestination.route) {
                        composable(OneDestination.route) { OneScreen() }
                        composable(TwoDestination.route) { TwoScreen() }
                        composable(FourDestination.route, arguments = FourDestination.arguments) {
                            val user = it.arguments?.getParcelable<User>(FourDestination.ARG)
                                ?: return@composable
                            FourScreen(user)
                        }
                        composable(ThreeDestination.route, arguments = ThreeDestination.arguments) {
                            val channelId =
                                it.arguments?.getString(ThreeDestination.ARG) ?: return@composable
                            ThreeScreen(channelId)
                        }

                        composable(FiveDestination.route, arguments = FiveDestination.arguments) {
                            val age =
                                it.arguments?.getInt(FiveDestination.ARG_AGE) ?: return@composable
                            val name =
                                it.arguments?.getString(FiveDestination.ARG_NAME)
                                    ?: return@composable
                            FiveScreen(age, name)
                        }
                    }
复制代码

导航

看下现在的导航是有多简单:

   Button(onClick = {
            AppNav.to(TwoDestination.route)
        }) {
            Text(text = "去TwoScreen")
        }
        Button(onClick = {
            AppNav.to(ThreeDestination("来自首页"))
        }) {
            Text(text = "去ThreeScreen")
        }
        Button(onClick = {
            AppNav.to(FourDestination(User("来着首页", "110")))
        }) {
            Text(text = "去FourScreen")
        }
        Button(onClick = {
            AppNav.to(FiveDestination(20, "来自首页"))
        }) {
            Text(text = "去FiveScreen")
        }
复制代码

sem título.gif

完成上述操作后,我们已经能够在模块化应用程序中实现 Jetpack Compose 导航。并且使我们能够集中导航逻辑,在这样做的同时,我们可以看到一系列优势:

  • 我们不再需要将 NavHostController 传递给我们的可组合函数,消除了我们的功能模块依赖于 Compose Navigation 依赖项的需要,同时还简化了我们的构造函数以进行测试。
  • Adicionamos ViewModelsuporte para navegar nas funções normais.
  • Simplifica operações como substituição e popping, e é simples de implementar em uma frase.

A navegação no Compose ainda está em seus estágios iniciais e, com as melhorias oficiais, talvez não precisemos de encapsulamento, mas por enquanto estou feliz com a forma como a implementei.

Publiquei este warehouse no Maven Central e você pode confiar diretamente nele:

implementation 'io.github.yuexunshi:Nav:1.0.1'
复制代码

Anexe o código-fonte

Acho que você gosta

Origin juejin.im/post/7155289564775448607
Recomendado
Clasificación