Jetpack Compose初体验--(导航、生命周期等)

·  阅读 2905
Jetpack Compose初体验--(导航、生命周期等)

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

上一篇Jetpack Compose初体验 中练习了Compose中的布局、自定义布局、自定义view、动画、手势等操作,这些都是在一个页面中完成,一个应用不能只有一个页面,今天来练习一下Jetpack Compose中的导航。

普通导航

在Jetpack Compose中导航可以使用Jetpack中的Navigation组件,引入相关的扩展依赖就可以了 Navigation官方文档

implementation "androidx.navigation:navigation-compose:2.4.0-alpha01"
复制代码

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

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

导航的路径使用字符串来表示,当使用NavController导航到某个页面的时候,NavHost内部会自动进行页面重组。

来个小栗子实践一下

@Composable
fun MainView(){
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "first_screen"){
        composable("first_screen"){
            FirstScreen(navController = navController)
        }
        composable("second_screen"){
            SecondScreen(navController = navController)
        }
        composable("third_screen"){
            ThirdScreen(navController = navController)
        }
    }
}
复制代码
  • 通过rememberNavController()方法创建navController对象
  • 创建NavHost对象,传入navController并指定首页
  • 通过composable()方法来往NavHost中添加页面,构造方法中的字符串就代表该页面的路径,后面的第二个参数就是具体的页面。

下面把这三个页面写出来,每个页面里面都有个按钮继续执行其他导航

@Composable
fun FirstScreen(navController: NavController){
    Column(modifier = Modifier.fillMaxSize().background(Color.Blue),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
        ) {
        Button(onClick = {
            navController.navigate("second_screen")
        }) {
            Text(text = "I am First 点击我去Second")
        }
    }
}
@Composable
fun SecondScreen(navController: NavController){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = {
            navController.navigate("third_screen")
        }) {
            Text(text = "I am Second 点击我去Third")
        }
    }
}
@Composable
fun ThirdScreen(navController: NavController){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = {
            navController.navigate("first_screen")
        }) {
            Text(text = "I am Third 点击我去first")
        }
    }
}
复制代码

这样一个简单的导航效果就完成了,感觉用了这个之后,要跟activity和fragment说拜拜了~~ ,全场只需一个activity加一堆可组合项(@Composable),新建一个页面简单了太多太多。

当然页面之间跳转传参是少不了的,Compose中如何传参呢?

参数传递肯定有发送端和接收端,navController是发送端,NavHost是接收端。先在NavHost中配置参数占位符,和接收取参数的方法。

@Composable
fun MainView(){
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "first_screen"){
        composable("first_screen"){
            FirstScreen(navController = navController)
        }
        composable("second_screen/{userId}/{isShow}",
            //默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type
        arguments = listOf(navArgument("isShow"){type = NavType.BoolType})
        ){ backStackEntry ->
            SecondScreen(navController = navController,
                backStackEntry.arguments?.getString("userId"),
                backStackEntry.arguments?.getBoolean("isShow")!!
            )
        }
        composable("third_screen?selectable={selectable}",
        arguments = listOf(navArgument("selectable"){defaultValue = "哈哈哈我是可选参数的默认值"})){
            ThirdScreen(navController = navController,it.arguments?.getString("selectable"))
        }
        composable("four_screen"){
            FourScreen(navController = navController)
        }
    }
}
复制代码

如上代码,接收参数直接在在该页面地址后面添加参数占位符类似second_screen/{userId}/{isShow},然后通过arguments参数来接收arguments = listOf(navArgument("isShow"){type = NavType.BoolType})。还可以通过defaultValue来定义参数的默认值。

默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type。

参数发送端更简单,参数直接跟到页面路径后面就可以,类似navController.navigate("second_screen/12345/true") 下面给前面的页面添加上参数

@Composable
fun FirstScreen(navController: NavController){
    Column(modifier = Modifier
        .fillMaxSize()
        .background(Color.Blue),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
        ) {
        Button(onClick = {
            navController.navigate("second_screen/12345/true"){
            }
        }) {
            Text(text = "I am First 点击我去Second")
        }
        Spacer(modifier = Modifier.size(30.dp))
    }
}
@Composable
fun SecondScreen(navController: NavController,userId:String?,isShow:Boolean){
    Column(modifier = Modifier
        .fillMaxSize()
        .background(Color.Green),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = {
            navController.navigate("third_screen?selectable=测试可选参数"){
                popUpTo(navController.graph.startDestinationId){saveState = true}
            }
        }) {
            Text(text = "I am Second 点击我去Third")
        }
        Spacer(modifier = Modifier.size(30.dp))
        Text(text = "arguments ${userId}")
        if(isShow){
            Text(text = "测试boolean值")
        }
    }
}
@Composable
fun ThirdScreen(navController: NavController,selectable:String?){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = {
            navController.navigate("first_screen")
        }) {
            Text(text = "I am Third 点击我去first")
        }
        Spacer(modifier = Modifier.size(30.dp))
        Button(onClick = {
            navController.navigate("four_screen")
        }) {
            Text(text = "I am Third 点击我去four")
        }
        selectable?.let { Text(text = it) }
    }
}
复制代码

效果如下

copmose_21.gif

生命周期

既然新的界面不使用activity或者fragment了,但是activity和fragment中的生命周期是非常有用的比如创建和销毁某些对象。那么Jetpack Compose中的每个组合函数的生命周期是怎样的呢?

可组合项的生命周期比视图比activity 和 fragment 的生命周期更简单,一般是进入组合、执行0次或者多次重组、退出组合。生命周期相关的函数主要有下面的几个,使用@Composable修饰的可组合函数中没有自带的生命周期函数,想要监听其生命周期,需要使用Effect API

  • LaunchedEffect:第一次调用Compose函数的时候调用
  • DisposableEffect:内部有一个 onDispose()函数,当页面退出时调用
  • SideEffect:compose函数每次执行都会调用该方法

来个小例子体验一下

@Composable
fun LifecycleDemo() {
    val count = remember { mutableStateOf(0) }

    Column {
        Button(onClick = {
            count.value++
        }) {
            Text("Click me")
        }

        LaunchedEffect(Unit){
                Log.d("Compose", "onactive with value: " + count.value)
            }
        DisposableEffect(Unit) {
            onDispose {
                Log.d("Compose", "onDispose because value=" + count.value)
            }
        }
        SideEffect {
            Log.d("Compose", "onChange with value: " + count.value)
        }
        Text(text = "You have clicked the button: " + count.value.toString())
    }
}
复制代码

效果如下:

copmose_26.gif

然后把前面的例子稍微改一下,我们把LaunchedEffect和DisposableEffect一起放到一个if语句里面

@Composable
fun LifecycleDemo() {
    val count = remember { mutableStateOf(0) }

    Column {
        Button(onClick = {
            count.value++
        }) {
            Text("Click me")
        }

        if (count.value < 3) {
            LaunchedEffect(Unit){
                Log.d("Compose", "onactive with value: " + count.value)
            }
            DisposableEffect(Unit) {
                onDispose {
                    Log.d("Compose", "onDispose because value=" + count.value)
                }
            }
        }
        
        SideEffect {
            Log.d("Compose", "onChange with value: " + count.value)
        }
        Text(text = "You have clicked the button: " + count.value.toString())
    }
}
复制代码

那么此时的生命周期就是:当首次进入if语句的时候执行LaunchedEffect函数,离开if语句的时候,就执行DisposableEffect方法。

底部导航

说到导航就不得不说底部导航和顶部导航,底部导航的实现非常简单,直接使用JetPack Compose提供的脚手架在结合navController和NavHost就能轻松实现

@Composable
fun BottomMainView(){
    val bottomItems = listOf(Screen.First,Screen.Second,Screen.Third)
    val navController = rememberNavController()
    Scaffold(
        bottomBar = {
            BottomNavigation {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route
                bottomItems.forEach{screen ->
                    BottomNavigationItem(
                        icon = { Icon(Icons.Filled.Favorite,"") },
                        label = { Text(stringResource(screen.resourceId)) },
                        selected = currentRoute == screen.route,
                        onClick = {
                            navController.navigate(screen.route){
                                //当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页
                                popUpTo(navController.graph.startDestinationId){saveState = true}
                                //从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例
                                launchSingleTop = true
                                //切换状态的时候保存页面状态
                                restoreState = true
                            }
                        })
                }

            }
        }
    ){
        NavHost(navController = navController, startDestination = Screen.First.route ){
            composable(Screen.First.route){
                First(navController)
            }
            composable(Screen.Second.route){
                Second(navController)
            }
            composable(Screen.Third.route){
                Third(navController)
            }
        }
    }
}
@Composable
fun First(navController: NavController){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "First",fontSize = 30.sp)
    }
}
@Composable
fun Second(navController: NavController){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Second",fontSize = 30.sp)
    }
}
@Composable
fun Third(navController: NavController){
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Third",fontSize = 30.sp)
    }
}
复制代码

效果如下

copmose_22.gif

顶部导航

顶部导航使用TabRow和ScrollableTabRow这两个组件,其内部都是由一个一个的Tab组件组成。TabRow是平分整个屏幕的宽度,ScrollableTabRow可以超出屏幕宽度并且可以滑动,用法都是一样。

@Composable
fun TopTabRow(){
    var state by remember { mutableStateOf(0) }
    var titles = listOf("Java","Kotlin","Android","Flutter")
    Column {
        TabRow(selectedTabIndex = state) {
            titles.forEachIndexed{index,title ->
                run {
                    Tab(
                        selected = state == index,
                        onClick = { state = index },
                        text = {
                            Text(text = title)
                        })
                }
            }
        }
        Column(Modifier.weight(1f)) {
            when (state){
                0 -> TopTabFirst()
                1 -> TopTabSecond()
                2 -> TopTabThird()
                3 -> TopTabFour()
            }
        }
    }
}
@Composable
fun TopTabFirst(){
    Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Java")
    }
}
@Composable
fun TopTabSecond(){
    Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Kotlin")
    }
}
@Composable
fun TopTabThird(){
    Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Android")
    }
}
@Composable
fun TopTabFour(){
    Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Flutter")
    }
}
复制代码

copmose_23.gif

上面只能实现点击每个Tab 切换不同的页面,如果我们想要实现类似我们在xml布局中的ViewPage+TabLayout的效果呢

在Jetpack中怎么实现ViewPage的效果呢,Google的github上提供了一个半官方的库名字叫pager:github.com/google/acco…

implementation "com.google.accompanist:accompanist-pager:0.13.0"
复制代码

该库目前还是实验性的,以后API都可能会修改,目前使用的时候需要使用@ExperimentalPagerApi注解标记。

@ExperimentalPagerApi
@Composable
fun TopScrollTabRow(){
    var titles = listOf("Java","Kotlin","Android","Flutter","scala","python")
    val scope = rememberCoroutineScope()
    var pagerState = rememberPagerState(
        pageCount = titles.size, //总页数
        initialOffscreenLimit = 2, //预加载的个数
        infiniteLoop = true, //无限循环
        initialPage = 0, //初始页面
    )
    Column {
        ScrollableTabRow(
            selectedTabIndex = pagerState.currentPage,
            modifier = Modifier.wrapContentSize(),
            edgePadding = 16.dp
        ) {
            titles.forEachIndexed{index,title ->
                run {
                    Tab(
                        selected = pagerState.currentPage == index,
                        onClick = {
                            scope.launch {
                                pagerState.scrollToPage(index)
                            }
                                  },
                        text = {
                            Text(text = title)
                        })
                }
            }
        }
        HorizontalPager(
            state=pagerState,
            modifier = Modifier.weight(1f)
        ) {index ->
            Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
                Text(text = titles[index])
            }
        }
    }
}
复制代码

pagerState.scrollToPage(index)方法可以控制pager滚动,不过它是一个suspend修饰的方法,需要运行在协程中,在jetpack compose中使用协程可以使用rememberCoroutineScope()方法来获取一个compose中的协程的作用域

效果如下:

copmose_24.gif

Banner

pager库都引入了那顺便吧Banner效果也练习一下,为了显示网络图片还得引入一个新的库,accompanist-coil。在JetPack Compose中官方提供了两个显示网络图片的库accompanist-coil和accompanist-glide,这里使用accompanist-coil。

implementation 'com.google.accompanist:accompanist-coil:0.11.1'
复制代码
@ExperimentalPagerApi
@Composable
fun Third(navController: NavController){
    var pics = listOf("https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png",
    "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
    "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
    "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png")
    Column(modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Third",fontSize = 30.sp)
        var pagerState = rememberPagerState(
            pageCount = 4, //总页数
            initialOffscreenLimit = 2, //预加载的个数
            infiniteLoop = true, //无限循环
            initialPage = 0, //初始页面
        )
        Box(modifier = Modifier
            .fillMaxWidth()
            .height(260.dp)
            .background(color = Color.Yellow)) {
            HorizontalPager(
                state=pagerState,
                modifier = Modifier.fillMaxSize()
            ) {index ->
                Image(modifier = Modifier.fillMaxSize(),
                    painter = rememberCoilPainter(request = pics[index]),
                    contentScale=ContentScale.Crop,
                    contentDescription = "图片描述")
            }
            HorizontalPagerIndicator(
                pagerState = pagerState,
                modifier = Modifier
                    .padding(16.dp).align(Alignment.BottomStart),
            )
        }
    }
}
复制代码

copmose_25.gif

使用Jetpack Compose写页面感觉比使用xml简单了很多,相信未来Android中的xml布局会像前端的jquary一样用的越来越少。

分类:
Android
分类:
Android