Jetpack Compose入门介绍

972 阅读6分钟

一、Jetpack Compose是个啥

Jetpack Compose是google提供的用于构建原生 Android 界面的新工具包:

  1. 使用kotlin api
  2. 使用响应式编程思想
  3. 采用完全声明式的代码编写方式

二、相较于原生View系统,Jetpack Compose有什么优点?

  1. 声明式 UI:使得UI代码更易于编写和维护,使用响应式编程思想自动更新UI,避免传统命令式手动更新容易出错的问题

  2. 函数式思想: Jetpack Compose采用函数式编程思想来构建UI,这意味着每个UI元素都是一个纯函数,由输入参数决定它的输出结果,这也让开发者可以更方便地测试UI组件

  3. 可组合性: Jetpack Compose允许开发者将UI组件拆分为较小的可组合部件,然后将它们组合成更大的UI组件。这样,开发者可以更容易地管理和共享UI代码

  4. 支持动态主题: Jetpack Compose支持动态更改应用程序的主题,包括颜色、字体大小等属性

  5. 更少的样板 代码: 相比于传统的Android View,Jetpack Compose需要编写的样板代码更少,因为它默认提供了许多常见的UI功能,如padding、margin、点击事件等

以下描述的Composable、组合、可组合项、可组合函数概念等价

三、怎么用?

1. 依赖引入

// Compose工具包,用于将原生android和Compose连接
implementation("androidx.activity:activity-compose:1.7.2")
// 支持Compose的material design库,包含ui样式,icon,动画等
implementation("androidx.compose.material3:material3")
// Compose基础库,包含compose编译器,运行时环境,基础ui组件等
implementation(platform("androidx.compose:compose-bom:2023.05.01"))
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.10"))

// 调试工具,比如Preview
debugImplementation("androidx.compose.ui:ui-tooling")

2. 可组合函数

使用@Composable注解的函数,使用双驼峰命名规则,函数名首字母大写,

@Composable
private fun ShowContent(dataList: List<Message>) {
    LazyColumn(
        modifier = Modifier
            .background(color = colorResource(id = R.color.lui_core_pure_white_100))
            .fillMaxSize()
    ) {
        items(dataList) {
            MessageCard(message = it)
        }
    }
}

可组合函数只能被另一个可组合函数调用,

3. 布局预览

developer.android.com/jetpack/com…

使用@Preview注解不带参的可组合函数,

@Preview(
    showSystemUi = true,
    device = Devices.NEXUS_10
)
@Composable
private fun PreviewShowContent() {
    ShowContent(SampleData.conversationSample)
}

我们强烈建议您不要向生产函数(即使其不带参数)添加 @Preview 注释,而是编写一个封装函数并在其中添加 @Preview 注释。

可以通过点击@Preview注解旁边的齿轮按钮配置预览参数,比如分辨率,横竖屏,日夜模式等

通过点击预览右上角的按钮,可以开启“可交互预览模式”。

4. 在Activity、Fragment中使用

Activity必须直接或间接继承ComponentActivity,因为需要使用到androidx.activity:activity-compose包中的ComponentActivity扩展函数来连接AndroidView和Compose

1. Activity中使用

class ComposeActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 可组合函数
            ComposeFragment()
        }
    }
}

上面setContentComponentActivity的扩展函数。

setContent内部调用的是Activity#setContentView(),所以需要在onCreate中调用,且只调用一次。

2. Fragment中使用

class DemoComposeFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                ShowContent(dataList = SampleData.conversationSample)
            }
        }
    }

androidx.compose.ui.platform.ComposeView间接继承自ViewGroup,是连接AndroidView和Compose的桥梁,通过调用setContent填充可组合项,用法和Activity中的一样。

3. 使用AndroidView在Activity中引入Fragment

androidx.compose.ui.viewinterop.AndroidView是一个可组合项,用于在可组合函数中提供一个可以复用的android原生View,

@ExperimentalComposeUiApi
@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    onReset: (T) -> Unit,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate,
    onRelease: (T) -> Unit = NoOpUpdate
)

所以可以通过factory构造一个包含了目标Fragment的View,这个View包含在AndroidView中,被需要的地方任意组合。

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ComposeFragment() {
    // 获取最近层级的Context
    val context = LocalContext.current
    val fragmentManager = (context as FragmentActivity).supportFragmentManager

    AndroidView(
        factory = {
            val fragment = DemoComposeFragment.newInstance()
            val container = FrameLayout(it).apply {
                id = R.id.id_compose_fragment_container
            }
            fragmentManager.beginTransaction().replace(container.id, fragment)
                .commitAllowingStateLoss()
            container
        },
        onReset = {
            Timber.d("AndroidView reset")
        },
        onRelease = {
            Timber.d("AndroidView release")
        },
        update = {
            Timber.d("AndroidView update")
        })
}

四、Composable

和原生android类似,Compose也提供了非常多的可组合项供组合出各种复杂界面,下面列举几个常用的可组合项

  • Text、Button、Image、Spacer、Divider
  • Column、Row、LazyX
  • Box、Surface

五、Modifier

developer.android.com/jetpack/com…

Modifier是一个装饰或者添加行为的有序的不变的集合。例如background、padding 、宽高、焦点点击事件等。或者给Text设置单行、给Button设置各种点击状态等行为。其实就是所有控件的通用属性都在Modifier中。

Text(
    modifier = Modifier
        .padding(3.dp)
        .width(20.dp)
        .height(6.dp)
        .background(color = colorResource(id = R.color.lui_core_lotus_100))
        .weight(1F),
    text = "Hello World",
    textAlign = TextAlign.Center,
    fontSize = 15.sp,
    color = Color.Black
)

如果没有background,直接使用padding的效果就是margin,

Text(
    modifier = Modifier
        .border(width = 2.dp, color = Color.Black)
        .padding( 20. dp)
        .background(color = colorResource(id = R.color.lui_core_lotus_100)),
    text = "Hello World",
    textAlign = TextAlign.Center,
    fontSize = 10.sp,
    color = Color.Black
)

Text(
    modifier = Modifier
        .border(width = 2.dp, color = Color.Black)
        .background(color = colorResource(id = R.color.lui_core_lotus_100))
        .padding( 20. dp) ,
    text = "Hello World",
    textAlign = TextAlign.Center,
    fontSize = 10.sp,
    color = Color.Black
)

六、状态管理

developer.android.com/jetpack/com…

由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新。其核心思想就是响应式编程

interface MutableState<T> : State<T> {
    override var value: T
}

使用mutableStateOf创建可观察的MutableState,如果 value 有任何变化,系统就会为用于读取 value 的所有可组合函数安排重组。

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       // by属性委托需要导入
       // import androidx.compose.runtime.getValue
       // import androidx.compose.runtime.setValue
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

remember关键字可以在重组后保持状态,但如果可组合项被移除,该状态会被忘记,比如列表中复用的可组合项。

七、动画

Modifier提供了一个组件大小动画animateContentSizeanimationSpec用来指定动画类型,

Surface(
    color = surfaceColor,
    modifier = Modifier
        .fillMaxWidth()
        .padding(top = 10.dp, end = 2.dp, bottom = 2.dp)
        .animateContentSize(
            animationSpec = spring(
                dampingRatio = Spring.DampingRatioHighBouncy,
                stiffness = Spring.StiffnessVeryLow
            )
        )
)

另外通用的做法是使用状态来做动画,sdk提供了以下状态动画,

以颜色动画为例,animationSpec用来指定动画类型

val surfaceColor by animateColorAsState(
    targetValue =
    if (message.isExpanded.value) {
        colorResource(id = R.color.lui_core_charging)
    } else {
        Color.Transparent
    },
    animationSpec = tween(durationMillis = 3000)
)

八、跨平台

github.com/JetBrains/c…

www.zhihu.com/question/30…

其他

  1. @Preview 用于预览UI,只能用作不带参Composable函数上
  2. 需要引用资源时,直接打“resource”,会有相关提示

  1. 如果没有background 直接使用padding的效果就是margin,pading和background一定要写在height和width前面不然会不起作用,也可以使用Spacer增加间距

  1. Box类似于FrameLayoutRelativeLayout,内容可叠加,大小默认wrap_content

  2. ColumnRow类似LinearLayoutScrollView的结合

  3. LazyColumnLazyRow类似RecyclerView

  4. Surface用于对子布局进行裁剪、边框、背景设置

  5. 所有的Composable大小默认自适应其内容大小

  6. Compose 与 Kotlin 的兼容性对应关系

  7. WebAssembly介绍

缺陷

  1. Preview预览效率低,不准确(和实际设备上的效果不一样),性能要求高
  2. Compose嵌套写法可读性不好,尤其是复杂界面,此时想快速找到某个组件比较依赖Preview