Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

Multiplatform

Compose Multiplatform 1.5.0 发布

Read this post in other languages:

Compose Multiplatform 1.5.0 现已正式推出。 它采用适用于 Kotlin 的 Jetpack Compose 声明式 UI 框架,并将其从 Android 扩展到桌面端、iOS 和 Web。 桌面版本已经稳定,iOS 处于 Alpha 阶段,Web 支持仍为实验性。 有关完整说明,请参阅 Compose Multiplatform 网站。 

此版本的一些亮点包括:

  1. Dialog、Popup 和 WindowInsets API 现在采用通用代码。
  2. 对于 iOS 滚动,资源管理和文本字段已得到改进。
  3. UI 测试框架在桌面端已经稳定。

此版本基于 Jetpack Compose 1.5,重点关注性能改进。 同时,它以 1.1 版 Material Design 3 为基础构建, 包括日期选择器和时间选择器等新组件。

试用 Compose Multiplatform 1.5.0

Compose Multiplatform 支持 Dialog、Popup 和 WindowInsets

从 1.5 版开始,Compose Multiplatform 中提供对话框和弹出窗口。 对话框用于模态事件,用户在其中做出选择或输入数据。 同时,弹出窗口用于非模态行为,例如提供可选功能。

在此版本中,基类型 DialogPopup,以及 DropdownMenuAlertDialog 都可以从通用代码中访问。 这避免了提供平台特定功能的需要。

例如,下面的可组合项完全以通用代码编写:

@Composable
fun CommonDialog() {
   var isDialogOpen by remember { mutableStateOf(false) }
   Button(onClick = { isDialogOpen = true }) {
       Text("Open")
   }
   if (isDialogOpen) {
       AlertDialog(
           onDismissRequest = { },
           confirmButton = {
               Button(onClick = { isDialogOpen = false }) {
                   Text("OK")
               }
           },
           title = { Text("Alert Dialog") },
           text = { Text("Lore ipsum") },
       )
   }
}

在桌面端、Android 和 iOS 上的显示方式:

桌面端对话框演示

Android 和 iOS 对话框演示

此版本提供的第三项功能是 WindowInsets API,描述为了防止内容与系统 UI 重叠而需要进行多少调整。 从版本 1.5 开始,此功能包含在 Compose Multiplatform 中,因此可在 Android 和 iOS 上使用。 

使用 WindowInsets API,可以通过 Compose Multiplatform 在凹口后绘制背景内容。 无需在应用程序顶部添加白线。 差异如以下屏幕截图所示:

使用 WindowInsets API 在 Compose Multiplatform 中绘制背景内容

iOS 上的改进

iOS 平台是此次发布的重点,包含大量改进。 滚动模仿了平台的外观和风格,资源管理得到简化,文本处理有所增强。

自然滚动

在此版本中,iOS 滚动已调整为模仿原生滚动。 假设代码中要显示的条目的数量和/或大小超出可用空间:

@Composable
fun NaturalScrolling() {
   val items = (1..30).map { "Item $it" }
   LazyColumn {
       items(items) {
           Text(
               text = it,
               fontSize = 30.sp,
               modifier = Modifier.padding(start = 20.dp)
           )
       }
   }
}

滚动时,条目会从屏幕边缘弹开,与原生 iPhone 应用程序相同:

iOS 上的滚动弹开

对动态字体的支持

iOS 上的动态字体功能允许用户设置偏好字体大小 – 大字体便于查看,小字体可容纳更多内容。 应用中使用的文本大小应与此系统设置相关。 

Compose Multiplatform 现在支持此功能。 缩放文本时使用的增量与原生应用程序中使用的增量相同,因此行为将相同。

以如下可组合项为例:

@Composable
fun DynamicType() {
   Text("This is some sample text", fontSize = 30.sp)
}

首选阅读大小设为最小时的显示画面:

Compose Multiplatform 中 iOS 上的动态字体功能(小文本)

这是首选阅读大小为最大时的结果:

Compose Multiplatform 中 iOS 上的动态字体功能(大文本)

对高刷新率显示屏的支持

在之前的版本中,最大帧率为 60 FPS。 这可能导致 UI 在 120Hz 屏幕的设备上缓慢且滞后。 从这个版本开始,支持的帧率最高为 120 FPS。

简化了资源管理

从 1.5.0 开始,iOS 源集的资源文件夹中的任何资源都会默认复制到应用程序捆绑包中。 例如,如果将图像文件放入 src/commonMain/resources/,它将被复制到捆绑包中并可从代码使用。

使用 CocoaPods 时,不再需要在 Gradle 构建文件中配置此行为。 您也不需要重新调用 podInstall 来确保资源在修改后被复制。 

从这个版本开始,如果您试图在构建脚本中显式配置行为(如下所示),您将收到错误:

kotlin {
    cocoapods {
        extraSpecAttributes["resources"] = "..."
    }
}

有关完整详细信息以及迁移现有代码的指南,请参阅此文档

改进了 TextField

早期版本中,在两种情况下输入文本可能导致意外行为。 从这个版本开始,增强的 TextField 已经克服了这些问题。

大小写问题

首先,TextField 现在可以识别首字母自动大写是否已禁用。 这在输入密码时尤其重要。 您可以通过 keyboardOptions 实参控制此行为。

为了说明这一点,请查看下面的可组合项: 

fun TextFieldCapitalization() {
   var text by remember { mutableStateOf("") }
   TextField(
       value = text,
       onValueChange = { text = it },
       keyboardOptions = KeyboardOptions(
           capitalization = KeyboardCapitalization.Sentences,
           autoCorrect = false,
           keyboardType = KeyboardType.Ascii,
       ),
   )
}

左图是大写属性设为 KeyboardCapitalization.None 时的情形,右图则显示了值为 KeyboardCapitalization.Sentences 时的情形。

TextField 大小写演示

硬件键盘

第二种情况与硬件键盘有关。 在以前的版本中,使用硬件键盘时,按 Enter 会导致多个换行符,按 Backspace 会触发多个删除。 从这个版本开始,这些事件可以正确处理。

桌面端改进

稳定了测试框架

此版本稳定了对 Compose for Desktop 测试的支持。 Jetpack Compose 提供了一组测试 API 来验证 Compose 代码的行为。 这些 API 先前已移植到桌面端并在之前的版本中可用,但存在限制。 这些限制现已移除,让您可以为应用程序编写全面的 UI 测试。

为了快速展示测试功能,我们来创建并测试一个简单的 UI。 下方是我们的示例可组合项:

@Composable
fun App() {
   var searchText by remember { mutableStateOf("cats") }
   val searchHistory = remember { mutableStateListOf() }


   Column(modifier = Modifier.padding(30.dp)) {
       TextField(
           modifier = Modifier.testTag("searchText"),
           value = searchText,
           onValueChange = {
               searchText = it
           }
       )
       Button(
           modifier = Modifier.testTag("search"),
           onClick = {
               searchHistory.add("You searched for: $searchText")
           }
       ) {
           Text("Search")
       }
       LazyColumn {
           items(searchHistory) {
               Text(
                   text = it,
                   fontSize = 20.sp,
                   modifier = Modifier.padding(start = 10.dp).testTag("attempt")
               )
           }
       }
   }
}

这将创建一个记录搜索尝试的简单 UI:

用于测试的搜索应用

请注意,Modifier.testTag 已用于为 TextFieldButtonLazyColumn 中的条目指定名称。 

然后,我们可以在 JUnit 测试中操作 UI:

class SearchAppTest {
   @get:Rule
   val compose = createComposeRule()


   @Test
   fun `Should display search attempts`() {
       compose.setContent {
           App()
       }


       val testSearches = listOf("cats", "dogs", "fish", "birds")


       for (text in testSearches) {
           compose.onNodeWithTag("searchText").performTextReplacement(text)
           compose.onNodeWithTag("search").performClick()
       }


       val lastAttempt = compose
           .onAllNodesWithTag("attempt")
           .assertCountEquals(testSearches.size)
           .onLast()


       val expectedText = "You searched for: ${testSearches.last()}"
       lastAttempt.assert(hasText(expectedText))
   }
}

使用特定于 Compose 的 JUnit 规则:

  1. 将 UI 的内容设置为应用可组合项。
  2. 通过 onNodeWithTag 查找文本字段和按钮。 
  3. 在文本字段中重复输入示例值,然后点击按钮。
  4. 通过 onAllNodesWithTag 查找生成的所有文本节点。
  5. 断言当前已创建的文本节点数,并获取最后一个。
  6. 断言最后一次尝试包含预期消息。

增强了 Swing 互操作性

此版本对 Swing 组件内 Compose 面板的改进呈现引入了实验性支持。 这可以防止在显示、隐藏或调整面板大小时出现过渡呈现问题。 它还支持在组合 Swing 组件和 Compose 面板时进行适当分层。 Swing 组件现在可以在 ComposePanel 上方或下方显示。

为了说明这一点,请查看下面的示例:

fun main() {
   System.setProperty("compose.swing.render.on.graphics", "true")
   SwingUtilities.invokeLater {
       val composePanel = ComposePanel().apply {
           setContent {
               Box(modifier = Modifier.background(Color.Black).fillMaxSize())
           }
       }


       val popup = object : JComponent() { ... }


       val rightPanel = JLayeredPane().apply {
           add(composePanel)
           add(popup)
           ...
       }


       val leftPanel = JPanel().apply { background = CYAN }


       val splitter = JSplitPane(..., leftPanel,rightPanel)


       JFrame().apply {
           add(splitter)
           setSize(600, 600)
           isVisible = true
       }
   }
}

在这段代码中,我们创建并显示一个 Swing JFrame,内容如下:

  1. JFrame 包含带有垂直分隔线的 JSplitPane
  2. 拆分窗格的左侧是青色的标准 JPanel
  3. 右侧是 JLayeredPane,由两层组成:
    • 包含 Box 可组合项的 ComposePanel,颜色为黑色
    • 自定义 Swing 组件,其中文本“Popup”出现在白色矩形内。 这通过重写 paintComponent 方法实现。

属性 compose.swing.render.on.graphics 设为 true 时: 

  • 自定义 Swing 组件显示在 Box 可组合项顶部。 
  • 移动滑块时不会出现过渡图形伪影。

Spring 互操作性演示正常运行

如果此标志未设置,则自定义组件将不可见,并且滑块移动时可能出现过渡伪影:

Spring 互操作性演示非正常运行

请分享您对 Compose Multiplatform 的反馈。 我们邀请您加入 Kotlin Slack #compose 频道,讨论与 Compose Multiplatform 和 Jetpack Compose 相关的一般主题。 在 #compose-ios 中,您可以找到有关 Compose Multiplatform for iOS 的讨论。

试用 Compose Multiplatform 1.5.0

更多文章和视频

本博文英文原作者:

image description

Discover more