Jetpack Compose 初体验(上)

你是否受够了 Android 中 UI 编写的体验——在 xml 文件中编写复杂的层级结构和繁多的属性,动态化的视图逻辑又被分裂到 Activity 中?哦,这该死的友好度和割裂感!html

这两年,Flutter 大行其道,不管是网上的讨论度仍是实际的落地项目,风头一时无两。因此从这个角度来讲,做为 UI 框架的 Flutter,无疑是成功的。本着借鉴的思想(或许吧,谁知道呢),Android 在 Jetpack 项目中新增了一套全新的视图开发套件——Compose。它有着和 Flutter 同样好看的(姑且这么认为吧)外表,但究竟只是一个好看的花瓶仍是才貌双全,这得咱们本身去寻找答案。前端

Compose 当前还处于测试版本,想要使用它,咱们须要首先下载 Android studio 的 canary 版本以提供支持。你能够在这里下载或者在你现有的 Android studio 中打开 File -> Settings -> Appearance & Behavior -> System Settings -> Updates 菜单,而后切换到 canary 渠道再点击 Check Now 按钮便可更新到最新的 canary 版本。android

1、布局

1.可组合函数

使用 Compose 建立一个界面是简单的,只需经过 @Composable 注解定义一个可组合函数,在函数中返回界面元素组件便可。web

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}
复制代码

该函数能够接收参数。函数内能够是一个组件,也能够是多个组件的组合。api

经过 setContent 方法块能够设置页面内容,相似于以前的 setContentView() 方法。markdown

setContent {
    Text(text = "Hello Compose!")
}
复制代码

相比于以前的界面书写过程,Compose 更「神奇」的一个体如今于它能够直接在 Android studio 中预览咱们编写的界面和组件,而无需让程序运行在设备中。网络

咱们只须要在可组合函数的基础上再新增一个 @Preview 注解,可是须要注意的是,预览函数不接受参数,因此比较好的作法是在可组合函数的基础上编写其对应的预览函数。数据结构

@Preview
@Composable
fun DefaultPreview() {
    Greeting("Android")
}
复制代码

预览函数对你的应用在设备上的最终呈现不会产生影响,Android studio 提供了一个预览窗口能够实时看到预览函数所呈现的效果。app

image-20210412225335487.png

2.布局

咱们编写的应用界面几乎任什么时候候都不会是简简单单的单一的控件,而是必定数量的独立控件在空间上的一种组合。框架

首先,咱们就盲猜,若是我想竖直方向排列三个文字组件,确定不是像下面这样随便组合三个 Text 控件。它怎么可能那么聪明,能知道你是想横着排仍是竖着排,想并排排仍是旋转开。怎么可能有人比苏菲更懂你!

@Composable
fun VerticalText() {
    Text("Hello World!")
    Text("Hello Again World!")
    Text("How old are you, World!")
}
复制代码

image-20210413003422129.png

那,就组合喽。

@Composable
fun VerticalText() {
    Column {
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
复制代码

给三个 Text 约定个竖框框,它们就能乖乖地排起队。

image-20210413005809783.png

这里,悄摸摸地说一句,这要是没有偷瞄 Flutter 的考卷 向优秀的思想借鉴,我把三个 Text 布局在我脑门上!

固然,只有这么生硬的排列可不行,咱们还须要加点属性,使得整个布局更和谐点——例如,加点边距。

咱们但愿给 Column 加一个内边距,那么咱们就应该给 Column 添加一个属性。Modifier 类用来给组件添加装饰或者行为,如背景、边距、点击事件等。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
复制代码

image-20210413210728702.png

如今,为了让界面看起来不那么单调,咱们给这个界面加上下面这一张图片。

![](Compose 初体验.assets/hello_world_new_black.png)

将这张图片拷贝到 drawable 资源文件夹下面,而后经过下面的方式引用。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null
        )
        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
复制代码

Image 的其中一个构造函数支持如下参数,其中 painter 参数和 contentDescription 参数没有默认值,为必传参数。

image-20210420220142848.png

这样,图片就被构造出来啦,看一下效果:

image-20210420220432840.png

那怎么该对图片进行一些约束呢?做为一个头图,我不但愿它这么哗众取宠,作图片要低调一点。

在上面,咱们认识了 Modifier,那就寻求它的帮助,让咱们的图片小一些吧。

Image(
    painter = painterResource(id = R.drawable.hello_world_new_black),
    contentDescription = null,
    modifier = Modifier
    	.width(126.dp)
    	.height(62.dp),
    contentScale = ContentScale.Inside
)
复制代码

借助 Modifier 将图片的高度和宽度分别进行限定。而后经过 contentScale 参数对图片的缩放方式进行约束。ContentScale.Inside 保持图片比例不变的状况下尽量地充满父控件的体积。

把上面的 Image 放入 preview 方法,看一下效果:

image-20210420221651702.png

如今头图就被咱们拿捏得死死的,可是它还不是很好看,没脖子,加个脖子。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
复制代码

image-20210420222219906.png

这样是否是好看多了,嗯,是的。

3.Material Design

谷歌霸霸的产品固然是支持 Material Design 的,那咱就看看。

作头图不要锋芒毕露,作图处事要圆滑一点。给头图加个圆角是个不错的想法。

在 Android 传统的 UI 编写中,圆角图片一直没有很简单的解决方案,须要经过诸如自定义 ImageView 的方式来实现。可是,朋友们,当你使用 Compose 框架的时候,只须要一行代码就能够圆角图片的显示!家祭无忘告乃翁。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp)
                .clip(shape = RoundedCornerShape(4.dp)),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!")
        Text("Hello Again World!")
        Text("How old are you, World!")
    }
}
复制代码

这里仍是经过 Modifier 来实现需求,怎么样,如今的头图是否是圆滑可爱了不少。

image-20210420223334597.png

头图这么求上进,文字也不能落后,一篇好的文章要主次分明,错落有致。

声明 Typography 对象,而后给 Text 添加 style 属性,来控制文字的样式。

@Preview(showBackground = true)
@Composable
fun VerticalText() {
    val typography = MaterialTheme.typography
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.hello_world_new_black),
            contentDescription = null,
            modifier = Modifier
                .width(126.dp)
                .height(62.dp)
                .clip(shape = RoundedCornerShape(4.dp)),
            contentScale = ContentScale.Inside
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text("Hello World!", style = typography.h3)
        Text("Hello Again World!", style = typography.body1)
        Text("How old are you, World!", style = typography.body2)
    }
}
复制代码

Typography 提供以下预设属性,囊括标题、子标题、段落体、按钮等。

image-20210420225524291.png

最终效果以下:

image-20210420225406851.png

怎么样,是否是主次开始变得分明了?结构变得清晰了?情节展开得顺滑了?故事开始天然了?……

固然,其余的诸如最大行数、字体、对齐方式等均可以被配置。

2、主题

基本布局已经差很少啦,那么咱们再来搞一些共性的东西,就像咱们黄种人都有同样的肤色——散在土地里的黄,有种顽强,很是东方……

之前的 View 系统其实也有关于 theme 的定义,那些被定义的 style,在官方定义的一系列 theme 的基础上加以扩展,造成咱们 app 的主题。

Compose 框架提供了 Material Design 的实现,Material Design Theme 天然也被应用到 Compose 中,Material Design Theme 包括了对颜色、文本样式和形状等属性的定义,我们自定义这些属性后,包括 button、cards、switches 等控件都会相应的改变它们的默认样式。

1.颜色

颜色在前端开发中真的是无处不在了,Color 能够帮助咱们快速地构建颜色模型。

你能够泡着吃:

val red = Color(0xffff0000)
复制代码

能够扭着吃:

val blue = Color(red = 0f, green = 0f, blue = 1f)
复制代码

欸,你还能够干吃:

val black = Color.Black
复制代码

只要你喜欢,你甚至能够空翻360度加转体一周半的时候吃:

// 我不会空翻,也不会转体,期待你的表现,加油!
复制代码

Compose 提供了 Colors数来建立成套的浅色或深色:

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

private val DarkColorPalette = darkColors(
        primary = Purple200,
        primaryVariant = Purple700,
        secondary = Teal200,
        onPrimary = Color.Green
)

private val LightColorPalette = lightColors(
        primary = Purple500,
        primaryVariant Customize= Purple700,
        secondary = Teal200,
        onPrimary = Color.Green

        /* Other default colors to override background = Color.White, surface = Color.White, onPrimary = Color.White, onSecondary = Color.Black, onBackground = Color.Black, onSurface = Color.Black, */
)
复制代码

而后,就能够传递给 MaterialTheme 使用喽:

@Composable
fun TestComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
            colors = colors,
            typography = Typography,
            shapes = Shapes,
            content = content
    )
}
复制代码

怎么样,还自动适配深色模式。

并且,咱们也能够随时随地获取到主题色:

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)
复制代码

表面颜色和内容颜色又是另外一个概念了,许多组件都接受一对颜色和「内容颜色」:

Surface(
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
    …

TopAppBar(
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    …
复制代码

这样一来,您不只能够设置可组合项的颜色,并且还能为包含在可组合项中的内容提供默认颜色。默认状况下,许多可组合项都使用这种内容颜色。例如,Text 的颜色基于其父项的内容颜色,而 Icon :「俺也同样」,它可使用该颜色来设置其色调。

contentColorFor() 方法能够为任何主题颜色检索适当的“on”颜色。例如,若是您设置 primary 背景,就会将 onPrimary 设置内容颜色。若是您设置非主题背景颜色,还应指定合理的内容颜色。使用 LocalContentColor 可检索与当前背景造成对比的当前内容颜色。

咱们以上面自定义的 Theme 来试验,使用它做为咱们的主题:

@Preview
@Composable
fun TestColor() {
    TestComposeTheme {
        Button(onClick = {}) {
            Text(
                "hello world"
            )
        }
    }
}Customize
复制代码

效果:

image-20210429183615573.png

2.字体排版

字体排版主要经过 TypographyTextStyle 类来完成。Typography 构造函数能够提供每种样式的默认值,所以您能够省略不但愿自定义的任何样式:

val Rubik = FontFamily(
    Font(R.font.rubik_regular),
    Font(R.font.rubik_medium, FontWeight.W500),
    Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
    h1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)
复制代码

若是您但愿自始至终使用同一字体,请指定 defaultFontFamily 参数,并省略全部 TextStyle 元素的 fontFamily

val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)
复制代码

使用时,能够从主题检索 TextStyle,如如下示例所示:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)
复制代码

3.形状

Compose 中能够轻松地定义各类形状,好比圆角或者操场跑道形状,在传统 View 系统中实现都比较麻烦。

咱们如今修改一下上面的 Button 的形状来看看效果:

val Shapes = Shapes(
        small = CutCornerShape(
                topStart = 16.dp,
                topEnd = 0.dp,
                bottomStart = 16.dp,
                bottomEnd = 0.dp
        ),
        medium = RoundedCornerShape(percent = 50),
        large = RoundedCornerShape(0.dp)
)
复制代码

image-20210429192726472.png

这里有一点须要注意的是,默认状况下,许多组件使用这些形状。例如,Button、TextField 和 FloatingActionButton 默认为 small,AlertDialog 默认为 medium,而 ModalDrawerLayout 默认为 large。如需查看完整的对应关系,请参阅形状方案参考文档。

3、列表

列表也是个常见的家伙,Android View 系统中早期的 ListView 和后来的 RecyclerView, Flutter 里的 ListView 等。

一个列表就是许多个元素排排站,整齐笔直。那一个纵向(或横向)的布局中动态地添加进许多的元素不就行了。

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}
复制代码

来,你猜,RecyclerView 是否是这么写的。这里有个最大的问题,假如你是个交际花,好友从这里排到法国,列表多到滑一夜滑不到头,那么一次加载是否是要耗费巨大的资源,搞很差卡死了王思聪联系不上你那就太不给面了,很差。

RecyclerView 最大的一个优势是它能够懒加载列表项,一次只加载一个屏幕的条目(四舍五入就是我对)。Compose 中可没有 RecyclerView,可是一样有针对这一问题优化的组件,LazyColumnLazyRow 是垂直和水平方向的懒加载列表控件。咱们先来看一下效果:

@Preview
@Composable
fun TestList() {
    LazyColumn(modifier = Modifier.fillMaxWidth(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)) {
        // Add a single item
        item {
            Text(text = "First item")
        }

        // Add 5 items
        items(10000) { index ->
            Text(text = "Item: $index")
        }

        // Add another single item
        item {
            Text(text = "Last item")
        }
    }
}
复制代码

这里加载了一万个元素的列表,看看这丝滑的效果吧(建议就着德芙食用)。

lazy_column.gif

咱们还能够像上面同样,经过 contentPadding 设置内容边距,verticalArrangement 则是能够设置 item 间间距,以及均匀地排列元素以充满父空间。

比较遗憾地是 LazyColumnLazyRow 暂时没法设置例如添加元素时地动画,期待后续的加入吧。

LazyColumn 能够轻松地实现粘性标题,只需使用 stickyHeader() 函数便可:

// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}
复制代码

上面的代码演示了如何经过 Map 数据结构实现粘性标题的数据展现。

既然有列表,那么确定会有宫格列表,LazyVerticalGrid 则可以帮助咱们实现需求。更多用法查看相关 API(没错,我就是 LazyBoy,但个人尊严决定了我不会滚)。

在实际项目开发中,咱们常常会遇到将数据分页展现的状况,以减小数据请求压力。借助 Paging 3.0 库 能够来进行分页,Paging 库是 Jetpack 中重要的一项新特性,可帮助您一次加载和显示多个小的数据块。按需载入部分数据会减小网络带宽和系统资源的使用量。

如需显示分页内容列表,可使用 collectAsLazyPagingItems() 扩展函数,而后将返回的 LazyPagingItems 传入 LazyColumn 中的 items()。与视图中的 Paging 支持相似,您能够经过检查 item 是否为 null,在加载数据时显示占位符:

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) { message ->
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}
复制代码
相关文章
相关标签/搜索