美文网首页
compose--附带效应、传统项目集成、导航

compose--附带效应、传统项目集成、导航

作者: aruba | 来源:发表于2022-12-15 16:47 被阅读0次

    该文章将是compose基础系列中最后一篇,附带效应是这篇文章的重点,其余补充内容为如何在传统xml中集成composecompose导航的使用

    一、附带效应

    有了前面的了解,我们知道compose中是由State状态发生改变来使得可组函数发生重组,状态的改变应该是在可组合函数作用域中,但有时我们需要它发生在别的作用域,如定时弹出一个消息,这就需要附带效应出场了,compose定义了一系列附带效应API,来运用在可组合函数作用域内外,发生状态改变的不同场景

    1.LaunchedEffect

    LaunchedEffect我们之前就已经使用过了,特别是在低级别动画时,LaunchedEffect用于安全地调用挂起函数,本质就是启动一个协程,LaunchedEffect的调用需要在可组合函数作用域内

    LaunchedEffect的执行分为以下三种,优先级由上到下:

    • 当发生重组时LaunchedEffect退出组合,将取消协程
    • 当发生重组时如果LaunchedEffect使用的同一个key,并且上次LaunchedEffect没执行结束,则不执行
    • 当发生重组时如果LaunchedEffect使用的不同的key,并且上次LaunchedEffect没执行结束,则取消上次执行,启动新的协程执行该次任务

    例子:

    @Preview
    @Composable
    fun MyLaunchEffect() {
        var state by remember { mutableStateOf(false) }
        var count by remember { mutableStateOf(0) }
    
        if (state) {
            // key为Unit唯一值
            LaunchedEffect(Unit) {
                delay(3000)
                count++
            }
        }
    
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Cyan)
            .clickable { state = !state }
        ) {
            Text("执行了${count}次")
        }
    }
    

    先是点击两下的效果,由于statefalse时,没有LaunchedEffect的代码块,此时LaunchedEffect会取消:

    稍微改变下例子的代码,一旦状态发生改变,那么重复执行LaunchedEffect

    @Preview
    @Composable
    fun MyLaunchEffect2() {
        var state by remember { mutableStateOf(0) }
        var count by remember { mutableStateOf(0) }
    
        if (state > 0) {
            // key为Unit唯一值
            LaunchedEffect(Unit) {
                delay(3000)
                count++
            }
        }
    
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Cyan)
            .clickable { state++ }
        ) {
            Text("执行了${count}次")
        }
    }
    

    点击三下的效果,LaunchedEffectkey唯一,并且重复执行,由于第一次没执行完,所以只会执行第一次的LaunchedEffect

    改变例子代码,每次执行的key不同:

    @Preview
    @Composable
    fun MyLaunchEffect3() {
        var state by remember { mutableStateOf(0) }
        var count by remember { mutableStateOf(0) }
    
        if (state > 0) {
            // key为随机值
            LaunchedEffect(UUID.randomUUID()) {
                delay(3000)
                // 置为0,防止不断重组导致一直执行LaunchedEffect
                state = 0
                count++
            }
        }
    
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Cyan)
            .clickable { state++ }
        ) {
            Text("执行了${count}次")
        }
    }
    

    效果,取消了之前的LaunchedEffect,隔了3秒后才发生count状态改变:

    2.rememberCoroutineScope

    rememberCoroutineScope也是使用过的,它返回一个remember的协程作用域,可以在可组合函数外使用,调用几次执行几次

    例子:

    @Preview
    @Composable
    fun MyRememberCoroutineScope() {
        val scope = rememberCoroutineScope()
        var count by remember { mutableStateOf(0) }
    
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Cyan)
            .clickable {
                scope.launch {
                    delay(3000)
                    count++;
                }
            }
        ) {
            Text("执行了${count}次")
        }
    }
    

    效果:

    3.rememberUpdatedState

    LaunchedEffect一旦启动,同一个key其内部的方法调用和引用都是final的,即无法更改,如果LaunchedEffect内使用的外部引用可能发生改变,应该使用rememberUpdatedState

    3.1 不使用remember

    先来看一个例子,我在重组时生成一个随机数,并作为onTimeout()的打印参数,将onTimeout()传给MyRememberUpdatedStateLaunchedEffect内调用onTimeout()打印这个随机数:

    @Preview
    @Composable
    fun MyTimeout() {
        var state by remember { mutableStateOf(false) }
    
        Column {
            // 1.生成随机数
            val random = Random.nextInt()
            Log.i("onTimeout", "return : $random")
            MyRememberUpdatedState(state) {
                // 4.打印随机数
                Log.i("onTimeout", "onTimeout() return : $random")
            }
    
            Button(onClick = { state = !state }) {
                Text("click")
            }
        }
    }
    
    @Composable
    fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
        // 使用rememberUpdatedState
    //    val rememberUpdatedState by rememberUpdatedState(onTimeout)
        val rememberUpdatedState = onTimeout
    
        // 2.key唯一发生重组,不会重新执行
        LaunchedEffect(true) {
            delay(5000)
            // 3.延迟5s,调用外部传入的onTimeout()
            rememberUpdatedState()
        }
    
        if (enable)
            Text("hi")
        else
            Text("hello")
    }
    

    我点击多次,这次的效果直接看日志即可:

    可以看到最后打印的结果,是第一次生成的随机数

    3.2 使用remember

    我们尝试使用remember,将onTimeout作为State状态并记住,并以onTimeout作为key使得每次onTimeout发生改变,触发值的更新:

    @Preview
    @Composable
    fun MyTimeout() {
        var state by remember { mutableStateOf(false) }
    
        Column {
            // 1.生成随机数
            val random = Random.nextInt()
            Log.i("onTimeout", "return : $random")
            MyRememberUpdatedState(state) {
                // 4.打印随机数
                Log.i("onTimeout", "onTimeout() return : $random")
            }
    
            Button(onClick = { state = !state }) {
                Text("click")
            }
        }
    }
    
    @Composable
    fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
        // 使用rememberUpdatedState
    //    val rememberUpdatedState by rememberUpdatedState(onTimeout)
        val rememberUpdatedState by remember(onTimeout) { mutableStateOf(onTimeout) }
    //    val rememberUpdatedState = onTimeout
    
        // 2.key唯一发生重组,不会重新执行
        LaunchedEffect(true) {
            delay(5000)
            // 3.延迟5s,调用外部传入的onTimeout()
            rememberUpdatedState()
        }
    
        if (enable)
            Text("hi")
        else
            Text("hello")
    }
    

    打印的结果,依然是第一次生成的随机数:

    3.3 使用rememberUpdatedState

    rememberUpdatedState可以始终保持最新的值,从而改变LaunchedEffect运行时的引用的值

    @Preview
    @Composable
    fun MyTimeout() {
        var state by remember { mutableStateOf(false) }
    
        Column {
            // 1.生成随机数
            val random = Random.nextInt()
            Log.i("onTimeout", "return : $random")
            MyRememberUpdatedState(state) {
                // 4.打印随机数
                Log.i("onTimeout", "onTimeout() return : $random")
            }
    
            Button(onClick = { state = !state }) {
                Text("click")
            }
        }
    }
    
    @Composable
    fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
        // 使用rememberUpdatedState
        val rememberUpdatedState by rememberUpdatedState(onTimeout)
    //    val rememberUpdatedState by remember{ mutableStateOf(onTimeout) }
    //    val rememberUpdatedState = onTimeout
    
        // 2.key唯一发生重组,不会重新执行
        LaunchedEffect(true) {
            delay(5000)
            // 3.延迟5s,调用外部传入的onTimeout()
            rememberUpdatedState()
        }
    
        if (enable)
            Text("hi")
        else
            Text("hello")
    }
    

    打印结果:

    原理:首先我们知道remember相当于创建了一个静态变量,如果不指定key,只会初始化一次,重复调用remember并不会更新引用,指定key时,当key发生变化,则会更新引用
    LaunchedEffect运行时会复制引用,新建变量指向传入的引用,所以此时无论外部变量的引用发生如何改变,并不会改变LaunchedEffect内部变量的引用
    rememberUpdatedStateremember的基础上做了更新值处理,每次调用到rememberUpdatedState时,将值更新,也就是引用的值的更新,此时不管外部变量还是LaunchedEffect内部变量的值引用都会发生变化,LaunchedEffect调用的自然就是最新的方法了,下面是rememberUpdatedState的源码:

    @Composable
    fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
        mutableStateOf(newValue)
    }.apply { value = newValue }
    

    4.DisposableEffect

    DisposableEffect可以在key变化和移除时做一些善后工作,需实现onDispose

    例子:

    @Preview
    @Composable
    fun MyDisposableEffect() {
        var state by remember { mutableStateOf(false) }
        var text by remember { mutableStateOf("click") }
        val scope = rememberCoroutineScope()
    
        if (state) {
            // 重组或移除时会调用onDispose
            DisposableEffect(Unit) {
                val job = scope.launch { 
                    delay(3000)
                    text = "点了"
                }
    
                onDispose {
                    job.cancel()
                    text = "取消了"
                }
            }
        }
    
        Button(onClick = { state = !state }) {
            Text(text)
        }
    }
    

    效果,在3s内点击了两次,导致重组时移除DisposableEffect而触发onDispose

    5.SideEffect

    SideEffect会在可组合函数重组完成时调用,可以进行用户行为分析、日志记录等操作

    例子:

    @OptIn(ExperimentalAnimationApi::class)
    @Preview
    @Composable
    fun MySideEffect() {
        var enable by remember { mutableStateOf(false) }
    
        Column {
            AnimatedVisibility(
                visible = enable,
                enter = scaleIn(tween(2000)),
                exit = scaleOut(tween(2000))
            ) {
                MySideEffectText("hello world")
            }
    
            Button(onClick = { enable = !enable }) {
                Text("click")
            }
        }
    }
    
    @Composable
    fun MySideEffectText(text: String) {
        SideEffect {
            Log.i("SideEffect", "重组完成")
        }
    
        Text(text)
    }
    

    效果,如果组件重组完成了,连续点击导致动画重复执行,则不会触发重组:

    6.produceState

    produceState 会启动一个协程,并返回一个State对象,用来将非 Compose 状态转换为 Compose 状态,即执行一些耗时操作,如网络请求,并将结果作为State对象返回

    例子:

    @Preview
    @Composable
    fun MyProduceState() {
        var visiable by remember { mutableStateOf(false) }
    
        Column {
            if (visiable)
                Text(load().value)
    
            Button(onClick = { visiable = !visiable }) {
                Text("load")
            }
        }
    }
    
    @Composable
    fun load(): State<String> {
        return produceState(initialValue = "", producer = {
            delay(2000);
    
            value = "hi"
        })
    }
    

    效果:

    7.derivedStateOf

    derivedStateOf可以将一个或多个状态对象转变为其他的状态对象,一旦状态发生改变,只会在用到该derivedStateOf状态的地方进行重组

    例子,根据传入的list,过滤高亮的元素,并展示到列表中:

    val alpha = arrayOf("a", "b", "c", "d", "e", "f", "g", "h")
    
    @Preview
    @Composable
    fun MyDerivedStateOf() {
        val items = remember { mutableStateListOf<String>() }
    
        Column {
            Button(onClick = { items.add(alpha[Random.nextInt(alpha.size)]) }) {
                Text("Add")
            }
    
            DerivedStateOf(items, highPriorityKeywords = listOf("a", "b"))
        }
    }
    
    /**
     * 拥有highPriorityKeywords的优先显示
     */
    @Composable
    fun DerivedStateOf(
        lists: List<String>,
        highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
    ) {
        // 需要高亮置顶的items
        val highPriorityLists by remember(highPriorityKeywords) {
            derivedStateOf { lists.filter { it in highPriorityKeywords } }
        }
    
        LazyColumn {
            items(highPriorityLists) { value ->
                Text(value, color = Color.Red)
            }
            items(lists) { value ->
                Text(value)
            }
        }
    }
    

    效果:

    8.snapshotFlow

    snapshotFlow可以将 ComposeState 转为Flow,当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值

    @Preview
    @Composable
    fun MySnapshotFlow() {
        val listState = rememberLazyListState()
        val list = remember {
            mutableListOf<Int>().apply {
                repeat(1000) { index ->
                    this += index
                }
            }
        }
    
        LazyColumn(state = listState) {
            items(list) {
                Text("hi:${it}")
            }
        }
    
        LaunchedEffect(Unit) {
            snapshotFlow {
                listState.firstVisibleItemIndex
            }.collect { index ->
                Log.i("collect", "${index}")
            }
        }
    }
    

    滚动查看日志:

    9.重启效应

    Compose 中有一些效应(如 LaunchedEffectproduceStateDisposableEffect)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。在实际开发中,灵活运用key是否唯一来使得是否需要重启效应

    二、传统项目集成

    官方推荐一次性替换整个布局,也可以替换部分布局,本身compose就兼容传统xml的方式,所以在传统的项目上集成compose很容易

    1.xml中使用compose

    xml中使用ComposeView,表示一个加载compose的控件:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ComposeIntegrateActivity">
    
        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="hello android"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/composeView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textView2" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    Activity中调用ComposeViewsetContent()方法,并使用compose:

    class ComposeIntegrateActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_compose_integrate)
    
            val composeView = findViewById<ComposeView>(R.id.composeView)
            composeView.setContent {
                MyComposeApplicationTheme {
                    MyText1()
                }
            }
        }
    
        @Composable
        fun MyText1() {
            Text("hi compose")
        }
    }
    

    启动效果:

    2.fragment中使用

    fragment中要多一步绑定View树生命周期:

    class BlankFragment : Fragment() {
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val disposeOnViewTreeLifecycleDestroyed =
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
            val root = inflater.inflate(R.layout.fragment_blank, container, false)
            root.findViewById<ComposeView>(R.id.fragment_composeView).apply {
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
                    MaterialTheme() {
                        // In Compose world
                        Text("Hello Compose!")
                    }
                }
            }
            return root
        }
    }
    

    三、导航

    compose定义了全新的导航API,下面来开始使用它

    1.导入依赖

        def nav_version = "2.5.3"
    
        implementation "androidx.navigation:navigation-compose:$nav_version"
    

    2.创建 NavHost

    NavHost需要一个navController用于控制导航到那个可组合项,startDestination 初始的可组合项,以及NavGraphBuilder导航关系图

    class NaviActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyComposeApplicationTheme {
                    MyNavi()
                }
            }
        }
    }
    
    @Preview
    @Composable
    fun MyNavi() {
        val navController = rememberNavController()
    
        NavHost(navController = navController, startDestination = "home") {
            composable("home") { Home() }
            composable("message") { Message() }
            composable("mine") { Mine() }
        }
    }
    
    @Composable
    fun Home() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Home")
        }
    }
    
    @Composable
    fun Message() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Message")
        }
    }
    
    @Composable
    fun Mine() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Mine")
        }
    }
    

    效果:

    3.navController

    接下来使用navController来导航到不同的可组合项,下面是官方给出的示例的几种方式:

    • 在导航到“friendslist”并加到返回堆栈中
    navController.navigate("friendslist")
    
    • 在导航到“friendslist”之前,将所有内容从后堆栈中弹出到“home”(不包含home)
    navController.navigate("friendslist") {
        popUpTo("home")
    }
    
    • 在导航到“friendslist”之前,从堆栈中弹出所有内容,包括“home”
    navController.navigate("friendslist") {
        popUpTo("home") { inclusive = true }
    }
    
    • 只有当我们还不在“search”时,才能导航到“search”目标地,避免在后堆栈的顶部有多个副本
    navController.navigate("search") {
        launchSingleTop = true
    }
    

    例子:

    我们给App添加上Scaffold,并在底部导航栏进行navController导航的控制

    class NaviActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyComposeApplicationTheme {
                    Scene()
                }
            }
        }
    }
    
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun Scene() {
        val navController = rememberNavController()
    
        Surface(Modifier.background(MaterialTheme.colorScheme.surface)) {
            Scaffold(
                topBar = {
                    TopAppBar(
                        title = {
                            Text(
                                stringResource(id = R.string.app_name),
                                color = MaterialTheme.colorScheme.onPrimaryContainer
                            )
                        },
                        colors = TopAppBarDefaults.smallTopAppBarColors(
                            containerColor = MaterialTheme.colorScheme.primaryContainer
                        )
                    )
                },
                bottomBar = {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(MaterialTheme.colorScheme.primaryContainer)
                            .padding(10.dp),
                        horizontalArrangement = Arrangement.SpaceAround
                    ) {
                        CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
                            Icon(
                                Icons.Rounded.Home, contentDescription = null,
                                modifier = Modifier.clickable {
                                    navController.navigate("home") {
                                        launchSingleTop = true
                                        popUpTo("home")
                                    }
                                }
                            )
                            Icon(
                                Icons.Rounded.Email, contentDescription = null,
                                modifier = Modifier.clickable {
                                    navController.navigate("message") {
                                        launchSingleTop = true
                                        popUpTo("message")
                                    }
                                }
                            )
                            Icon(
                                Icons.Rounded.Face, contentDescription = null,
                                modifier = Modifier.clickable {
                                    navController.navigate("mine") {
                                        launchSingleTop = true
                                        popUpTo("mine")
                                    }
                                }
                            )
                        }
                    }
                }
            ) { paddings ->
                MyNavi(
                    modifier = Modifier.padding(paddings),
                    navController = navController,
                    startDestination = "home"
                ) {
                    composable("home") { Home() }
                    composable("message") { Message() }
                    composable("mine") { Mine() }
                }
            }
        }
    }
    
    @Composable
    fun MyNavi(
        modifier: Modifier = Modifier,
        navController: NavHostController,
        startDestination: String,
        builder: NavGraphBuilder.() -> Unit
    ) {
        NavHost(
            modifier = modifier,
            navController = navController,
            startDestination = startDestination
        ) {
            builder()
        }
    }
    
    @Composable
    fun Home() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Home")
        }
    }
    
    @Composable
    fun Message() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Message")
        }
    }
    
    @Composable
    fun Mine() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text("Mine")
        }
    }
    

    效果:

    4.参数传递

    Navigation Compose 还支持在可组合项目的地之间传递参数,方式为Restful风格,这种风格的参数为必填:

    MyNavi(
    modifier = Modifier.padding(paddings),
    navController = navController,
    startDestination = "home/b1254"
    ) {
        composable("home/{userId}") { Home() }
        composable("message/{count}") { Message() }
        composable("mine/{userId}") { Mine() }
    }
    
    ...
    
    // 导航时带入参数
    navController.navigate("mine/a1587")
    

    参数类型默认为字符串,也可以通过navArgument指定参数的类型:

    composable(
    "home/{userId}",
    arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) { Home() }
    

    通过 lambda 中提供的NavBackStackEntry中提取这些参数:

    composable(
    "home/{userId}",
    arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {navBackStackEntry ->
        navBackStackEntry.arguments?.getString("userId")
        Home()
    }
    

    可选参数可以使用:?argName={argName} 来添加:

    composable(
    "message?count={count}",
    arguments = listOf(navArgument("count") {
        type = NavType.IntType
        defaultValue = 0
    })
    ) { Message() }
    

    5.深层链接

    深层链接照搬了官方文档:深层链接

    如果你想要将特定的网址、操作或 MIME 类型与导航绑定,实现对外提供跳转应用的功能,那么使用深层链接可以很方便的实现这个功能

    url为例,通过deepLinksurl进行绑定:

    val uri = "https://www.example.com"
    
    composable(
        "profile?id={id}",
        deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
    ) { backStackEntry ->
        Profile(navController, backStackEntry.arguments?.getString("id"))
    }
    

    manifest中注册配置:

    <activity …>
      <intent-filter>
        ...
        <data android:scheme="https" android:host="www.example.com" />
      </intent-filter>
    </activity>
    

    外部通过PendingIntent进行跳转:

    val id = "exampleId"
    val context = LocalContext.current
    val deepLinkIntent = Intent(
        Intent.ACTION_VIEW,
        "https://www.example.com/$id".toUri(),
        context,
        MyActivity::class.java
    )
    
    val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
        addNextIntentWithParentStack(deepLinkIntent)
        getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
    }
    

    6.封装导航图

    随着业务的越来越复杂,导航图也可能分为模块化,可以在NavHost作用域中使用navigation进行封装:

    NavHost(navController, startDestination = "home") {
        ...
        // Navigating to the graph via its route ('login') automatically
        // navigates to the graph's start destination - 'username'
        // therefore encapsulating the graph's internal routing logic
        navigation(startDestination = "username", route = "login") {
            composable("username") { ... }
            composable("password") { ... }
            composable("registration") { ... }
        }
        ...
    }
    

    使用扩展函数将更好的对模块进行封装:

    fun NavGraphBuilder.loginGraph(navController: NavController) {
        navigation(startDestination = "username", route = "login") {
            composable("username") { ... }
            composable("password") { ... }
            composable("registration") { ... }
        }
    }
    
    NavHost(navController, startDestination = "home") {
        ...
        loginGraph(navController)
        ...
    }
    

    相关文章

      网友评论

          本文标题:compose--附带效应、传统项目集成、导航

          本文链接:https://www.haomeiwen.com/subject/bnyhqdtx.html