服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

2 小时入门 Jetpack Compose (下) | 开发者说·DTalk

日期: 来源:Android 开发者收集编辑:朱涛


本文原作者: 朱涛原文发布于: 朱涛的自习室

在上一篇文章《 2 小时入门 Jetpack Compose (上) 》里,我们已经完成了 Splash 页面的 UI 和动画了。

这篇文章,让我们来完成:「首页」+「详情页」吧。



首页



跟 Splash 页面不一样,ToDoApp 的首页,要复杂不少。从结构上来讲,它主要分为三个部分:

  • 第一,页面顶部的 TopBar


  • 第二,页面的主要内容 Content,也就是 "待完成的任务列表";


  • 第三,页面右下角的 FloatingActionButton

看起来确实复杂不少,对吧?不过,借助 Compose 的 Scaffold,我们其实能快速实现这样的页面结构。
// 代码段 1
@Composablefun HomeScreen() { Scaffold( scaffoldState = scaffoldState, // 1 topBar = { HomeAppBar() }, // 2 content = { HomeContent() }, // 3 floatingActionButton = { HomeFab() } )}
// 1@Composablefun HomeAppBar() {}
// 2@Composablefun HomeContent() {}
// 3@Composablefun HomeFab() {}
万丈高楼平地起,虽然 HomeScreen 完整的代码有 500 多行,但它最基础的结构,上面的十多行代码就能概括。这都要感谢 Google 官方给我们提供的 Scaffold() 函数:
// 代码段 2
@Composablefun Scaffold( modifier: Modifier = Modifier, scaffoldState: ScaffoldState = rememberScaffoldState(), topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, isFloatingActionButtonDocked: Boolean = false, drawerContent: @Composable (ColumnScope.() -> Unit)? = null, drawerGesturesEnabled: Boolean = true, drawerShape: Shape = MaterialTheme.shapes.large, drawerElevation: Dp = DrawerDefaults.Elevation, drawerBackgroundColor: Color = MaterialTheme.colors.surface, drawerContentColor: Color = contentColorFor(drawerBackgroundColor), drawerScrimColor: Color = DrawerDefaults.scrimColor, backgroundColor: Color = MaterialTheme.colors.background, contentColor: Color = contentColorFor(backgroundColor), content: @Composable (PaddingValues) -> Unit) {}
可以看到,Scaffold() 支持的参数非常多,我们只是用到了它很小的一部分,它不仅支持 TopBar、FloatingActionButton,还支持 Drawer、BottomBar,这些都是开箱即用的,我们只需要做少许配置即可。这也是 XML 无法比拟的。

整个首页的骨架已经完成了,接下来,我们一起看看各个组件如何实现吧。


TopBar


首先,是首页顶部的 TopBar。

如果您只是想在 TopBar 上展示一个静态的 Title 的话,那是非常容易的。不过,这里我增加了一个快捷的「清空任务」操作。
只要用户点击这个「清空任务」的按钮,就会清空当前所有的任务。为了做到这一点,我们就需要传一个回调进来,这样方便数据层的操作。这也是 Hoisting 思想。这里,我们可以看做是 "事件提升"。
// 代码段 3
@Composablefun HomeAppBar( onDeleteAllConfirmed : () -> Unit) { HomeTopAppBar( onDeleteAllConfirmed = { onDeleteAllConfirmed() } )}
有了 Hosting 奠定基础以后,后面就很容易了,借助 Google 提供的 TopAppBar() 我们轻松就能实现。
// 代码段 4
@Composablefun HomeTopAppBar( onDeleteAllConfirmed: () -> Unit) { TopAppBar( // 1,Title:任务列表 title = { Text( text = stringResource(id = R.string.list_screen_title), color = MaterialTheme.colors.topAppBarContent ) }, // 2,选项:清空任务 actions = { HomeAppBarActions( onDeleteAllConfirmed = onDeleteAllConfirmed ) }, backgroundColor = MaterialTheme.colors.topAppBarBackground )}
@Composablefun HomeAppBarActions( onDeleteAllConfirmed: () -> Unit) { DeleteAllAction(onDeleteAllConfirmed = { isShowDialog = true })}
可以看到,Title 的展示很简单,只是读取了一下 String 而已。而对应的「清空任务」选项,我们则需要通过 DropdownMenu 来实现。
// 代码段 5
@Composablefun DeleteAllAction( onDeleteAllConfirmed: () -> Unit) { var expanded by remember { mutableStateOf(false) }
IconButton( onClick = { expanded = true } ) { // 1,更多按钮 Icon( painter = painterResource(id = R.drawable.ic_more), contentDescription = stringResource(id = R.string.delete_all_action), tint = MaterialTheme.colors.topAppBarContent ) // 2,下拉列表 DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { // 3,下拉列表,「清空任务」 DropdownMenuItem( onClick = { expanded = false onDeleteAllConfirmed() } ) { Text( modifier = Modifier .padding(start = MEDIUM_PADDING), text = stringResource(id = R.string.delete_all_action), style = Typography.subtitle2 ) } } }}
不过,由于「清空任务」是一个非常危险的操作,为了防止用户误操作,我们需要让用户「二次确认」。这时候,我们需要在 Compose 当中实现一个弹窗才行
在从前的 View 体系当中,弹窗是非常容易的。这在 Compose 当中也并不难,但实现方式却不太一样。
@Composablefun TodoAlertDialog(    title: String,    msg: String,    isShowDialog: Boolean,    // 1    onNoClicked: () -> Unit,  // 2    onYesClicked: () -> Unit, // 3) {    if (isShowDialog) {       // 4        AlertDialog(            title = {                Text(                    text = title,                    fontSize = MaterialTheme.typography.h5.fontSize,                    fontWeight = FontWeight.Bold                )            },            text = {                Text(                    text = msg,                    fontSize = MaterialTheme.typography.subtitle1.fontSize,                    fontWeight = FontWeight.Normal                )            },            confirmButton = {                Button(                    onClick = {                        onYesClicked()                        onNoClicked()                    })                {                    Text(text = stringResource(id = R.string.yes))                }            },            dismissButton = {                OutlinedButton(onClick = { onNoClicked() })                {                    Text(text = stringResource(id = R.string.no))                }            },            onDismissRequest = { onNoClicked() }        )    }}
请留意上面的注释 1、2、3,这里其实也再次体现了 Hoisting 的思想,这里不仅包含「事件提升」,还包含了「状态提升」的思想。

另外,请留意注释 4,发现了吗?在 Compose 当中,我们是通过控制状态 (isShowDialog) 来实现弹窗的「展示」与「隐藏」的。这跟我们 View 体系的 Dialog.show()、Dialog.hide() 的逻辑是不一样的。

至此,我们首页的 TopBar 就算完成了,接下来我们看看「任务列表」如何实现吧。


任务列表


首页的任务列表,它实现起来,其实要比传统的 RecyclerView 简单不少。

首先,我们需要判断一下,当前的任务列表是否为空。
@Composablefun HomeContent(    allTasks: RequestState<List<Task>>,    onSwipeToDelete: (Task) -> Unit,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit) {    if (allTasks is RequestState.Success &&        allTasks.data.isNotEmpty()    ) {        // 不为空        HomeTasksColumn()    } else {        // 空页面        HomeEmptyContent()    }}
空页面没什么好讲的,就是画个简单的 UI 页面而已,我们重点看看 HomeTasksColumn()。

如果我们只是想要单纯的展示任务列表的话,几行代码就可以搞定了。
@Composablefun HomeTasksColumn1(    tasks: List<Task>,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit,) {    LazyColumn {        itemsIndexed(            items = tasks,            key = { _, task ->                task.id            }        ) { index, task ->            TaskItem(                task = task,                gotoTaskDetail = gotoTaskDetail,                onUpdateTask = onUpdateTask            )        }    }}
如果是从前的 View 体系,我们不仅要写 XML,还要写 LayoutManager、Adapter、数据绑定、更新,哎,想想都觉得烦。

Compose 迷人的地方在于,它强大的动画 API。简单的几行代码,我们就可以实现一些炫酷的效果。比如,这个进场的动效。
如果让您在 RecyclerView 上完美实现一个类似的效果,您要花多长时间?3 天?还是 3 小时?如果是在 Compose 当中,我只需要 1 分钟
@Composablefun HomeTasksColumn1(    tasks: List<Task>,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit,) {    LazyColumn {        itemsIndexed(            items = tasks,            key = { _, task ->                task.id            }        ) { index, task ->            val size = remember(tasks) {                tasks.size            }            // 省略部分代码            AnimatedVisibility(                visible = true,                enter = slideInHorizontally(                    animationSpec = tween(                        durationMillis = 300                    ),                    // index 越大,初始偏移越大                    initialOffsetX = { -(it * (index + 1) / (size + 2)) }                ),                exit = shrinkVertically(                    animationSpec = tween(                        durationMillis = 300                    )                )            ) {                TaskItem(                    task = task,                    gotoTaskDetail = gotoTaskDetail,                    onUpdateTask = onUpdateTask                )            }        }    }}
我只能说,Compose 真的太强了。

OK,「进场动效」有了,如果我们想实现「侧滑删除」的功能呢?

嗯…… 给我一首歌的时间吧~
@Composablefun HomeTasksColumn(    tasks: List<Task>,    onSwipeToDelete: (Task) -> Unit,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit,) {    LazyColumn {        itemsIndexed(            items = tasks,            key = { _, task ->                task.id            }        ) { index, task ->            val size = remember(tasks) {                tasks.size            }
// 省略部分
AnimatedVisibility( visible = itemAppeared && !isDismissed, enter = slideInHorizontally( animationSpec = tween( durationMillis = 300 ), initialOffsetX = { -(it * (index + 1) / (size + 2)) } ), exit = shrinkVertically( animationSpec = tween( durationMillis = 300 ) ) ) { // 1,变化 SwipeToDismiss( state = dismissState, directions = setOf(DismissDirection.EndToStart), dismissThresholds = { FractionalThreshold(fraction = 0.2f) }, background = { SwipeBackground(degrees = degrees) }, dismissContent = { TaskItem( task = task, gotoTaskDetail = gotoTaskDetail, onUpdateTask = onUpdateTask ) } ) } } }}
请留意注释 1,「侧滑删除」这个功能,官方已经帮我们封装好了,只需要使用 SwipeToDismiss() 即可。

不过,我们怎么会满足于这种默认效果呢?

让我们来做个炫酷的「侧滑动效」吧~
这个效果看着好像挺麻烦,但这在 Compose 当中只是小菜一碟
@Composablefun HomeTasksColumn(    tasks: List<Task>,    onSwipeToDelete: (Task) -> Unit,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit,) {    LazyColumn {        itemsIndexed(            items = tasks,            key = { _, task ->                task.id            }        ) { index, task ->            // 省略部分
// 1,变化 val degrees by animateFloatAsState( if (dismissState.targetValue == DismissValue.Default) 0f else -180f )
// 省略部分
AnimatedVisibility() { } } }}
在我们手指拖动 Item 的时候,垃圾桶图标会做一个「倾倒动画」,这本质上就是一个旋转动画而已。一个 animateFloatAsState{} 就能搞定了。

不过,在侧滑的过程中,我们还希望 Toast 提示用户,什么时候可以松手,这该怎么办呢?

其实也不难,我们看看代码:
@Composablefun HomeTasksColumn(    tasks: List<Task>,    onSwipeToDelete: (Task) -> Unit,    gotoTaskDetail: (taskId: Int) -> Unit,    onUpdateTask: (Task) -> Unit,) {    LazyColumn {        itemsIndexed(            items = tasks,            key = { _, task ->                task.id            }        ) { index, task ->            // 省略部分
// 1 val isDeleteEnable by remember(degrees) { derivedStateOf { degrees == -180f } }
val context = LocalContext.current
// 2 DisposableEffect(key1 = isDeleteEnable) { if (isDeleteEnable) { showToast(context, "松手后删除!") } onDispose {} }
// 省略部分
AnimatedVisibility() { } } }}

App 运行期间,Composable 方法是会被反复调用的,我们需要 isDeleteEnable 来标记用户是否触发了「侧滑删除」的阈值。这里我们使用了 derivedStateOf 来优化 Compose 的性能


另外,为了防止 Recompose 的时候一直弹 Toast,我们使用了 DisposableEffect 这个 SideEffect Handler。这也是「函数式编程」领域的老概念了。

OK,任务列表完成以后,首页基本上就没什么难度了,FloatingActionButton 的代码我就不贴了。

接下来,我们来看看「详情页」怎么写。



详情页



有了「首页」的经验以后,「详情页」的实现就简单了,借助 Scaffold() 我们可以轻松实现页面的骨架。
@Composablefun TaskDetailScreen() {    Scaffold(        topBar = {            TaskDetailAppBar()        },        content = {            TaskDetailContent()        }    )}
这次我就不再介绍 TopBar 的实现了,我们直接看 TaskDetailContent() 吧。
可以看到,整个「详情页」的结构其实很简单,它只有两排:
  • 第一排,包含一个 TextField、CheckBox;
  • 第二排,还是一个 TextField。
@Composablefun TaskDetailContent() {    Column() {        Row(modifier = Modifier.fillMaxWidth()) {            TextField()            Checkbox()        }
Divider() TextField() }}
OK,搞清楚它的结构以后,剩下的就是完善 UI 的细节了:
@Composablefun TaskDetailContent(    modifier: Modifier = Modifier,    title: String,    onTitleChange: (String) -> Unit,    description: String,    onDescriptionChange: (String) -> Unit,    isDone: Boolean,    isDoneChange: (Boolean) -> Unit,) {    Column(        modifier = modifier            .fillMaxSize()            .background(MaterialTheme.colors.background)            .padding(all = MEDIUM_PADDING)    ) {
Row(modifier = Modifier.fillMaxWidth()) { TextField( modifier = Modifier .fillMaxWidth() .weight(8F), value = title, onValueChange = { onTitleChange(it) }, label = { Text(stringResource(id = R.string.enter_title)) }, placeholder = { Text(text = stringResource(id = R.string.title)) }, textStyle = MaterialTheme.typography.body1, singleLine = true, colors = TextFieldDefaults.textFieldColors( backgroundColor = Transparent ) )
Checkbox( modifier = Modifier.weight(1F), checked = isDone, onCheckedChange = isDoneChange ) }
Divider( modifier = Modifier.height(SMALL_PADDING), color = MaterialTheme.colors.background )
TextField( modifier = Modifier .fillMaxWidth() .height(200.dp), value = description, onValueChange = { onDescriptionChange(it) }, label = { Text(stringResource(id = R.string.enter_description)) }, placeholder = { Text(text = stringResource(id = R.string.description)) }, textStyle = MaterialTheme.typography.body1, ) }}
是不是很简单?



结束语



恭喜!您已经完成课程 80% 的内容了。


接下来需要做的,就是跟随文章里的步骤,一行行的敲代码了。读文章只需要 10 分钟,只有真正花 2 小时写代码,才可能:「2 小时入门 Jetpack Compose」!





长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。




 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 




相关阅读

  • 第一个ChatGPT单!赚了3K!

  • 最近,我在疯狂测试ChatGPT。有让我惊喜的地方,也有让我无语的地方。写代码的速度提高了。无语的地方,有的很简单的问题回答的五花八门。回答不准确的问题,确实训练下就好多了。
  • 滨州一人被骗28万!

  •   近日,阳信县公安局成功将两名涉嫌帮助信息网络犯罪活动罪的犯罪嫌疑人抓获归案。3月19日,阳信县一居民报案称:接到一陌生电话,对方以抖音点赞点关注做任务为由,将其拉入一QQ
  • 6 个令人惊艳的 ChatGPT 项目,开源了!

  • 公众号关注 “GitHubDaily”设为 “星标”,每天带你逛 GitHub!过去一周,技术圈的各个爆炸新闻,可以说是让我真正见证到了什么叫人间一日,AI 一年。首先是 New Bing 对所有用户放
  • 源代码与二进制漏洞的融合

  • 本文为看雪论坛优秀文章看雪论坛作者ID:TUGOhost一摘要反编译器是用来从程序二进制恢复到高级语言表示(通常是C代码)的工具。在过去的五年中,反编译器有了很大的改进,不仅是在产
  • ChatGPT引入插件策略,AI迎来AppStore时刻

  • 点击蓝字 关注我们Touch JiangsuNow to follow当地时间 3 月 23 日,OpenAI 宣布正式上线以安全为核心的 ChatGPT 插件系统。OpenAI 插件将 ChatGPT 连接到第三方应用程序,之
  • 一种非侵入式幂等性的Java实现

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。作者:llsydn 链接:https://juejin.cn/post/7085944825085689864 今天我们来谈谈什么是幂等性?引用百度百科的解析如下:幂等
  • 稷下新论|正岩:克服一切困难完成任务

  • 克服一切困难完成任务□正岩发展的道路上,不可能一帆风顺、一路坦途、一蹴而就,必然会有涉滩之险、爬坡之艰、闯关之难。面对困难,坚定信心,保持定力,全力攻坚,定能解决困难。当前
  • 详解,机器视觉软件开发SDK

  • 点击下方卡片,关注“新机器视觉”公众号重磅干货,第一时间送达 其实很简单,SDK 就是 Software Development Kit 的缩写,中文意思就是“软件开发工具包”。这是一个覆盖面相

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 2 小时入门 Jetpack Compose (下) | 开发者说·DTalk

  • 本文原作者: 朱涛,原文发布于: 朱涛的自习室在上一篇文章《 2 小时入门 Jetpack Compose (上) 》里,我们已经完成了 Splash 页面的 UI 和动画了。这篇文章,让我们来完成:「首页
  • 由浅入深了解 APK 构建流程

  • 概述APK构建流程涉及许多将项目转换成 Android 应用软件包 (APK) 的工具和流程。构建流程非常灵活,因此了解它的一些底层工作原理会很有帮助。APK的详细构建流程稍微有点复杂