前言

炼体系列的文章是针对有一定Compose根底的同学阅览和学习的,主要是结合Compose根底知识,完成各种常用界面,一般不会再去涉及到根底理论知识了,重点在怎么完成上。

假如你对Compose感兴趣可是是一个新手的话,主张先把握了Compose编程思维,搞懂重组,状况,附带效应等根底知识再来阅览会更好。根底入门的系列文章主张先看看筑基系列和过渡系列。

鬼话Compose筑基(1) – ()
鬼话Compose筑基(2) – ()
鬼话Compose筑基(3) – ()
鬼话Compose筑基(4) – ()
鬼话Compose筑基(5) – ()


鬼话Compose过渡篇(1) – ()
鬼话Compose过渡篇(2) – ()

假定咱们有个全新项目,准备用Compose的方法来完成页面。在开端前是不是会有很多疑问❓例如:

  • 怎么遵循app单Activity的架构理念?
  • Fragment还需求么?
  • 生命周期怎么操控?
  • 协程怎么跟生命周期绑定?
  • 页面间怎么传值?
  • 页面怎么跳转?
  • viewmodel怎么运用?
  • 依靠注入怎么运用?
  • 页面怎么兼容xml?
  • 等等更多

,这些现在都不必管!完全不必管!这些疑问在后面自然而然就都能找到答案了。所以先放下这些疑问,斗胆的迈出第一步才是要害!

假如你现在正在看什么Fragment最新api、databinding爽歪歪、recycleview相关之类的文章的话,主张你在体系学习Compose的时分先停一下,因为这些内容并不会对你现在的Compose学习有帮助反而会禁闭你的思维。

东西

根底布局字典
M3组件字典
组件演示和标准
以上3个链接包括了常用的布局和组件的运用介绍以及展示,有时刻的能够先学习了解作为储备,没有时刻的能够在需求用到的时分再去查询学习。

开端

接着上面,假定咱们要用Compose开端一个全新的项目。而大部分android app都有类似如下图的主页规划。包括顶部的top app bar(顶部栏),中心的主要内容 (例如列表),底部的navigation bar(导航栏),侧边的navigation drawer(侧面导航抽屉),右下角的FAB(悬浮按钮),不是常驻可见的或许还包括底部显现的snackbar和bottom sheet这类的控件。

大话Compose炼体(1)-先一餐吃3碗饭

在xml方法下,咱们能够在xml布局文件里边去挑选用相应的布局和控件去嵌套组合出这样的一个页面。然后再经过id去获取相应的view去完成相关数据填充,以及交互。哪怕再不写任何逻辑的情况下,v层的代码行数估量都不少了。

既然大部分app都有会有这样一个差不多的主页规划,因此Compose供给了一个可自定义的Scaffold(主页脚手架)帮助咱们快速完成以上功用。

就像刚刚介绍到的,主页会用到的布局和组件仍是有不少的。所以炼体就从完成主页的Scaffold开端,作为咱们炼体的起点,经过Scaffold来一个一个的打开一些布局和控件的详细运用。

Scaffold

简单看一下Scaffold源码,能够看到Scaffold本身也是一个组合函数,经过在Surface布局下的ScaffoldLayout组合函数完成类包括fab,topbar,bottombar,snackbar等控件的丈量和绘制。现在咱们知道这些就够了。

@ExperimentalMaterial3Api
@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable () -> Unit = {},
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    containerColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = contentColorFor(containerColor),
    contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
    content: @Composable (PaddingValues) -> Unit
) {
    Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
        ScaffoldLayout(
            fabPosition = floatingActionButtonPosition,
            topBar = topBar,
            bottomBar = bottomBar,
            content = content,
            snackbar = snackbarHost,
            contentWindowInsets = contentWindowInsets,
            fab = floatingActionButton
        )
    }
}

topBar

topBar经常用top app bar填充,比如咱们也用它来演示。现在假定咱们完全不知道什么是top app bar,咱们该怎么做呢?还记得前面的东西里边的三个链接吧?首要经过组件展示的链接来看看什么是top app bar,点击检查什么是Top app bar
能够看到这样的一张图

大话Compose炼体(1)-先一餐吃3碗饭

经过这图是不是直观的知道了什么是top app bar?感兴趣的话能够再把其他内容阅览一下,这样咱们能够从规划的角度了解更多的运用标准等内容。可是这儿咱们经过这张图知道了top app bar有4种样式就够了。

然后咱们要知道怎么运用top app bar,这时分经过M3组件字典链接去搜top app bar,能够看到如下内容

大话Compose炼体(1)-先一餐吃3碗饭

正好对应了刚说到的4种样式的对应类。点击后跳转到相应的代码运用介绍。

CenterAlignedTopAppBar为例

大话Compose炼体(1)-先一餐吃3碗饭

然后咱们代码去详细完成一下如下:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, false)
    setContent {
        val systemUiController = rememberSystemUiController()
        val useDarkIcons = !isSystemInDarkTheme()
        DisposableEffect(systemUiController, useDarkIcons) {
            systemUiController.setSystemBarsColor(
                color = Color.Transparent,
                darkIcons = useDarkIcons,
            )
            onDispose {}
        }
        WaTheme {
            Scaffold(
                topBar = {
                    CenterAlignedTopAppBar(
                        title = {
                            Text(text = "主页")
                        },
                        navigationIcon = {
                            IconButton(onClick = { /*TODO*/ }) {
                                Icon(imageVector = Filled.Menu, contentDescription = "菜单")
                            }
                        },
                        actions = {
                            IconButton(onClick = { /* doSomething() */ }) {
                                Icon(
                                    imageVector = Filled.Person,
                                    contentDescription = "Localized description",
                                )
                            }
                        },
                    )
                },
            ) { padding ->
                padding
            }
        }
    }
}

仍是运用过渡篇咱们生成的主题样式,运转后页面如图:

大话Compose炼体(1)-先一餐吃3碗饭
,这儿没有处理左面导航按钮和右边动作按钮的点击事件,下面先来完成点击导航按钮弹出侧边导航抽屉的操作。

后面说到的组件或许布局就不会再重复怎么经过东西了解和运用的过程嘞,不知道的时分请照着top app bar 的比如自主查询了解学习。

NavigationDrawer

运用侧边导航抽屉组件需求留意一下,新版的compose m3库现已把这个组件的优先级提高了,而官方文档没有相应的更新。之前用过的xdm应该知道,之前要运用它是放在Scaffold里边的drawerContent{}槽内,可是新版发现现已没有这个槽了。经过源码发现,这个组件现在不以槽的形式出现了。而是能够不经过Scaffold直接完成NavigationDrawer组件,或许在NavigationDrawer的content{}块里边嵌套Scaffold,又或许在Scaffold的content代码块里边嵌套NavigationDrawer

留意!前两者是让抽屉在content{}块里边的页面的上层(抽屉拉出的时分会盖住topbar等),而后者会让抽屉在topBar,bottomBar的下层也就是跟Scaffold的content{}的页面平层(抽屉拉出会被topbar等隐瞒)。一般咱们都是选用前两者的完成方法,特殊情况会考虑最终一种方法。

现在咱们把WaTheme下的代码改动一下,完成点击导航按钮弹出侧边导航抽屉的操作,看看作用

大话Compose炼体(1)-先一餐吃3碗饭

代码如下

WaTheme {
    val scope = rememberCoroutineScope()
    val drawerState = rememberDrawerState(DrawerValue.Closed)
    val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
    val selectedItem = remember { mutableStateOf(items[0]) }
    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            ModalDrawerSheet(modifier = Modifier.fillMaxWidth(0.8f)) {
                Spacer(Modifier.height(12.dp))
                Text(
                    text = "标题",
                    Modifier.padding(28.dp),
                    style = MaterialTheme.typography.headlineSmall,
                )
                items.forEachIndexed { index, item ->
                    when (index) {
                        0 -> {
                            NavigationDrawerItem(
                                icon = { Icon(item, contentDescription = null) },
                                label = { Text("收藏文章") },
                                selected = item == selectedItem.value,
                                onClick = {
                                    scope.launch { drawerState.close() }
                                    selectedItem.value = item
                                },
                                badge = {
                                    Text(text = "123篇")
                                },
                                modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                            )
                        }
                        1 -> {
                            NavigationDrawerItem(
                                icon = { Icon(item, contentDescription = null) },
                                label = { Text("粉丝") },
                                selected = item == selectedItem.value,
                                onClick = {
                                    scope.launch { drawerState.close() }
                                    selectedItem.value = item
                                },
                                badge = {
                                    Text(text = "3人")
                                },
                                modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                            )
                        }
                        else -> {
                            NavigationDrawerItem(
                                icon = {
                                    BadgedBox(
                                        badge = {
                                            Badge {
                                                val badgeNumber = "8"
                                                Text(
                                                    badgeNumber,
                                                    modifier = Modifier.semantics {
                                                        contentDescription =
                                                            "$badgeNumber new notifications"
                                                    },
                                                )
                                            }
                                        },
                                    ) {
                                        Icon(item, contentDescription = null)
                                    }
                                },
                                label = { Text("未读邮件") },
                                selected = item == selectedItem.value,
                                onClick = {
                                    scope.launch { drawerState.close() }
                                    selectedItem.value = item
                                },
                                modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                            )
                        }
                    }
                }
            }
        },
        content = {
            // icons to mimic drawer destinations
            Scaffold(
                topBar = {
                    CenterAlignedTopAppBar(
                        title = {
                            Text(text = "主页")
                        },
                        navigationIcon = {
                            IconButton(
                                onClick = {
                                    scope.launch { drawerState.open() }
                                },
                            ) {
                                Icon(
                                    imageVector = Filled.Menu,
                                    contentDescription = "菜单",
                                )
                            }
                        },
                        actions = {
                            IconButton(
                                onClick = {
                                    Toast.makeText(
                                        this@MainActivity,
                                        "action icon clicked",
                                        Toast.LENGTH_SHORT,
                                    ).show()
                                },
                            ) {
                                Icon(
                                    imageVector = Filled.Person,
                                    contentDescription = "Localized description",
                                )
                            }
                        },
                    )
                },
            ) { _ ->
            }
        },
    )
}

能够看到ModalNavigationDrawer的drawerContent{}块里边放ModalDrawerSheet来完成了抽屉内容,ModalDrawerSheet源码简单看了后发现是经过一个Column垂直的列表布局来完成的,所以咱们能够在里边经过其他的组件+NavigationDrawerItem来完成咱们想要的抽屉内容。NavigationDrawerItem里边的labeliconbadge都是可组合项的函数类型参数,所以咱们在这儿能够进一步的自定义抽屉里边的每一个Item。

bottomBar

bottomBar一般用bottom app bar或许navigation bar填充,咱们今日挑选用navigation bar来演示。

Scaffold下面加上下面的代码

bottomBar = {
    NavigationBar {
        navigationBarItems.forEachIndexed { index, item ->
            NavigationBarItem(
                icon = { Icon(Icons.Filled.Favorite, contentDescription = item) },
                label = { Text(item) },
                selected = navigationBarSelectedItem == index,
                onClick = { navigationBarSelectedItem = index }
            )
        }
    }
}

作用如图

大话Compose炼体(1)-先一餐吃3碗饭

接下来咱们完成点击top app bar 右边的动作按钮弹出snackbar

snackBar

首要改动top app bar的action{}块下的点击事件

actions = {
    IconButton(
        onClick = {
            //showSnackbar是挂起函数,用scope开启一个协程运转
            scope.launch {
                //返回一个成果
                val showSnackbar =
                    snackbarHostState.showSnackbar(
                        "当时没有用户登录,是否登录?",
                        "确认",
                        true,
                    )
                 //经过成果条件触发后续操作
                when (showSnackbar) {
                    Dismissed -> {
                        Toast.makeText(
                            this@MainActivity,
                            "取消后躲藏snackbar",
                            Toast.LENGTH_SHORT,
                        ).show()
                    }
                    ActionPerformed -> {
                        Toast.makeText(
                            this@MainActivity,
                            "跳转到登录页面",
                            Toast.LENGTH_SHORT,
                        ).show()
                    }
                }
            }
        },
    ) {
        Icon(
            imageVector = Filled.Person,
            contentDescription = "Localized description",
        )
    }
}

在WaTheme{}下声明状况

val snackbarHostState = remember { SnackbarHostState() }

Scaffold{}下给snackbarHost赋值

snackbarHost = {
    SnackbarHost(hostState = snackbarHostState)
}

运转后snackbar能够经过点击icon button显现了,可是这儿有个问题。假如咱们在snackbar显现时,多次点击icon button,会启动多个起程,在当时snackbar的协程结束后会进入下一个协程,页面又会弹出snackbar,这个显然是不符合咱们预期的。

大话Compose炼体(1)-先一餐吃3碗饭

优化思路也很简单,就是在click的时分先判别snackbar是否显现中,假如显现中就躲藏,不然就显现。snackbar的显隐状况咱们能够经过snackbarHostState.currentSnackbarData是否为null来判别,假如snackbar当时没有显现,snackbarHostState.currentSnackbarData==null,反之!=null。

给代码加上判别

scope.launch {
    if (snackbarHostState.currentSnackbarData == null) {
        val showSnackbar =
            snackbarHostState.showSnackbar(
                "当时没有用户登录,是否登录?",
                "确认",
                true,
            )
        when (showSnackbar) {
            Dismissed -> {
                Toast.makeText(
                    this@MainActivity,
                    "取消后躲藏snackbar",
                    Toast.LENGTH_SHORT,
                ).show()
            }
            ActionPerformed -> {
                Toast.makeText(
                    this@MainActivity,
                    "跳转到登录页面",
                    Toast.LENGTH_SHORT,
                ).show()
            }
        }
    }else{
        snackbarHostState.currentSnackbarData?.dismiss()
    }
}

,再运转就符合预期了。

FAB

FAB的完成比较简单。需求知道的是Compose有4种FAB,别离SmallFABFABLargeFABExtendedFAB根据场景挑选运用。另外Compose M3现已不再支持之前M2的那种Docker方法放置FAB了。

floatingActionButton = {
    FloatingActionButton(
        onClick = { /* do something */ },
    ) {
        Icon(Icons.Filled.Add, "Localized description")
    }
}

最终咱们经过增加fab的点击来操控bottom sheet的显隐。

bottomSheet

完好代码

        setContent {
            val systemUiController = rememberSystemUiController()
            val useDarkIcons = !isSystemInDarkTheme()
            DisposableEffect(systemUiController, useDarkIcons) {
                systemUiController.setSystemBarsColor(
                    color = Color.Transparent,
                    darkIcons = useDarkIcons,
                )
                onDispose {}
            }
            WaTheme {
                val scope = rememberCoroutineScope()
                val drawerState = rememberDrawerState(DrawerValue.Closed)
                //声明一个状况操控bottom sheet可组合项的生命周期
                var openBottomSheet by rememberSaveable { mutableStateOf(false) }
                //声明一个状况来操控bottom sheet是否半打开
                var skipHalfExpanded by remember { mutableStateOf(false) }
                //声明一个sheet状况操控bottom sheet显隐
                val bottomSheetState = rememberSheetState(skipHalfExpanded)
                val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
                val selectedItem = remember { mutableStateOf(items[0]) }
                var navigationBarSelectedItem by remember { mutableStateOf(0) }
                val navigationBarItems = listOf("主页", "发现", "喜爱", "我")
                val snackbarHostState = remember { SnackbarHostState() }
                ModalNavigationDrawer(
                    drawerState = drawerState,
                    drawerContent = {
                        ModalDrawerSheet(modifier = Modifier.fillMaxWidth(0.8f)) {
                            Spacer(Modifier.height(12.dp))
                            Text(
                                text = "标题",
                                Modifier.padding(28.dp),
                                style = MaterialTheme.typography.headlineSmall,
                            )
                            items.forEachIndexed { index, item ->
                                when (index) {
                                    0 -> {
                                        NavigationDrawerItem(
                                            icon = { Icon(item, contentDescription = null) },
                                            label = { Text("收藏文章") },
                                            selected = item == selectedItem.value,
                                            onClick = {
                                                scope.launch { drawerState.close() }
                                                selectedItem.value = item
                                            },
                                            badge = {
                                                Text(text = "123篇")
                                            },
                                            modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                                        )
                                    }
                                    1 -> {
                                        NavigationDrawerItem(
                                            icon = { Icon(item, contentDescription = null) },
                                            label = { Text("粉丝") },
                                            selected = item == selectedItem.value,
                                            onClick = {
                                                scope.launch { drawerState.close() }
                                                selectedItem.value = item
                                            },
                                            badge = {
                                                Text(text = "3")
                                            },
                                            modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                                        )
                                    }
                                    else -> {
                                        NavigationDrawerItem(
                                            icon = {
                                                BadgedBox(
                                                    badge = {
                                                        Badge {
                                                            val badgeNumber = "8"
                                                            Text(
                                                                badgeNumber,
                                                                modifier = Modifier.semantics {
                                                                    contentDescription =
                                                                        "$badgeNumber new notifications"
                                                                },
                                                            )
                                                        }
                                                    },
                                                ) {
                                                    Icon(item, contentDescription = null)
                                                }
                                            },
                                            label = { Text("未读邮件") },
                                            selected = item == selectedItem.value,
                                            onClick = {
                                                scope.launch { drawerState.close() }
                                                selectedItem.value = item
                                            },
                                            modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
                                        )
                                    }
                                }
                            }
                        }
                    },
                    content = {
                        Scaffold(
                            topBar = {
                                CenterAlignedTopAppBar(
                                    title = {
                                        Text(text = "主页")
                                    },
                                    navigationIcon = {
                                        IconButton(
                                            onClick = {
                                                scope.launch { drawerState.open() }
                                            },
                                        ) {
                                            Icon(
                                                imageVector = Filled.Menu,
                                                contentDescription = "菜单",
                                            )
                                        }
                                    },
                                    actions = {
                                        IconButton(
                                            onClick = {
                                                scope.launch {
                                                    if (snackbarHostState.currentSnackbarData == null) {
                                                        val showSnackbar =
                                                            snackbarHostState.showSnackbar(
                                                                "当时没有用户登录,是否登录?",
                                                                "确认",
                                                                true,
                                                            )
                                                        when (showSnackbar) {
                                                            Dismissed -> {
                                                                Toast.makeText(
                                                                    this@MainActivity,
                                                                    "取消后躲藏snackbar",
                                                                    Toast.LENGTH_SHORT,
                                                                ).show()
                                                            }
                                                            ActionPerformed -> {
                                                                Toast.makeText(
                                                                    this@MainActivity,
                                                                    "跳转到登录页面",
                                                                    Toast.LENGTH_SHORT,
                                                                ).show()
                                                            }
                                                        }
                                                    } else {
                                                        snackbarHostState.currentSnackbarData?.dismiss()
                                                    }
                                                }
                                            },
                                        ) {
                                            Icon(
                                                imageVector = Filled.Person,
                                                contentDescription = "Localized description",
                                            )
                                        }
                                    },
                                )
                            },
                            bottomBar = {
                                NavigationBar {
                                    navigationBarItems.forEachIndexed { index, item ->
                                        NavigationBarItem(
                                            icon = {
                                                Icon(
                                                    Icons.Filled.Favorite,
                                                    contentDescription = item,
                                                )
                                            },
                                            label = { Text(item) },
                                            selected = navigationBarSelectedItem == index,
                                            onClick = { navigationBarSelectedItem = index },
                                        )
                                    }
                                }
                            },
                            snackbarHost = {
                                SnackbarHost(hostState = snackbarHostState)
                            },
                            floatingActionButton = {
                                FloatingActionButton(
                                    onClick = {
                                         //经过fab的点击改动状况,触发重组时bottom sheet进入组合或许退出组合的逻辑
                                        openBottomSheet = !openBottomSheet
                                    },
                                ) {
                                    Icon(Icons.Filled.Add, "Localized description")
                                }
                            },
                        ) { _ ->
                                Row(
                                    Modifier.fillMaxSize().toggleable(
                                        value = skipHalfExpanded,
                                        role = Role.Checkbox,
                                        onValueChange = { checked ->
                                            skipHalfExpanded = checked
                                        },
                                    ),
                                    horizontalArrangement = Arrangement.Center,
                                    verticalAlignment = Alignment.CenterVertically,
                                ) {
                                    Checkbox(
                                        checked = skipHalfExpanded,
                                        onCheckedChange = null,
                                    )
                                    Spacer(Modifier.width(16.dp))
                                    Text("跳过半打开状况")
                                }
                            if (openBottomSheet) {
                                ModalBottomSheet(
                                    //bottom sheet显现时点击外部回调,改动状况让bottom sheet退出组合,完成躲藏
                                    onDismissRequest = { openBottomSheet = false },
                                    sheetState = bottomSheetState,
                                ) {
                                    Row(
                                        Modifier.fillMaxWidth(),
                                        horizontalArrangement = Arrangement.Center,
                                    ) {
                                        Button(
                                            //留意:这个当地很要害,假如咱们在onDismissRequest外处理bottom sheet的躲藏,
                                            // hide后必须要改动影响触发bottom sheet组合的状况(openBottomSheet)
                                            //hide后一定还要openBottomSheet =false,这样之前的BottomSheet就会退出组合
                                            //完成真正意义上的躲藏(不然组合项的布局仍然会覆盖整个屏幕)
                                            onClick = {
                                                scope.launch { bottomSheetState.hide() }
                                                    .invokeOnCompletion {
                                                        if (!bottomSheetState.isVisible) {
                                                            openBottomSheet = false
                                                        }
                                                    }
                                            },
                                        ) {
                                            Text("躲藏bottom sheet")
                                        }
                                    }
                                    LazyColumn {
                                        items(50) {
                                            ListItem(
                                                headlineText = { Text("Item $it") },
                                                leadingContent = {
                                                    Icon(
                                                        Icons.Default.Favorite,
                                                        contentDescription = "Localized description",
                                                    )
                                                },
                                            )
                                        }
                                    }
                                }
                            }
                        }
                    },
                )
            }
        }

咱们来看一下全体作用
)

小结

本篇经过凭借Scaffold来快速完成一个主页,在过程中举例展示了一些常见布局和组件的运用,并且特意以top app bar为例演示了怎么经过东西文档来自我探索学习

授人以鱼不如授人以渔,Compose M3的组件和布局不少,版别也比较靠前,每次迭代的变动仍是挺大的,所以一个一个得去介绍去分析,那是浪费咱们大家的精力和时刻。主要的是咱们要有学习才能和探索精力,前期需求用到哪个布局组件就去学习它,完成它,这样堆集一段时刻后,进步是很快的。假如永远都是看他人写,听他人说,牛逼的也永远是他人不是自己。

强烈主张花个半响时刻,跟着敲一下,根底有不懂得去看看之前的筑基篇或许评论留言。一天下来你会发现 你真的变强了~