文件/媒体与互操作
CMP 开发中最常见的痛点就是:如何选择文件?如何处理外部拖入的文件?如何播放视频?这些通常涉及平台特定 API。
1. 跨平台文件选择 (File Picker)
由于各个平台的文件选择器实现完全不同(Android 是 Activity Result API,Desktop 是 JFileChooser/Swing),我们需要使用 expect/actual 模式封装。
定义接口 (commonMain)
kotlin
// FilePicker.kt (commonMain)
expect class FilePickerLauncher(
onFileSelected: (File?) -> Unit
) {
fun launch()
}
@Composable
expect fun rememberFilePickerLauncher(
onFileSelected: (File?) -> Unit
): FilePickerLauncherAndroid 实现 (androidMain)
Android 需要使用 ActivityResultLauncher。
kotlin
// FilePicker.android.kt
actual class FilePickerLauncher(
private val launcher: ActivityResultLauncher<String>
) {
actual fun launch() {
launcher.launch("*/*") // 选择所有类型
}
}
@Composable
actual fun rememberFilePickerLauncher(
onFileSelected: (File?) -> Unit
): FilePickerLauncher {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
// 这里需要将 Uri 转换为通用 File 对象或直接读取流
// 建议仅传递 Uri String 或 InputStream
onFileSelected(uri?.let { File(it.toString()) })
}
return remember { FilePickerLauncher(launcher) }
}Desktop 实现 (desktopMain)
Desktop 可以直接调用 Java AWT/Swing 的组件。
kotlin
// FilePicker.desktop.kt
import java.awt.FileDialog
import java.awt.Frame
actual class FilePickerLauncher(
private val onFileSelected: (File?) -> Unit
) {
actual fun launch() {
val dialog = FileDialog(null as Frame?, "Select File", FileDialog.LOAD)
dialog.isVisible = true
val file = if (dialog.file != null) {
File(dialog.directory, dialog.file)
} else null
onFileSelected(file)
}
}
@Composable
actual fun rememberFilePickerLauncher(
onFileSelected: (File?) -> Unit
): FilePickerLauncher {
return remember { FilePickerLauncher(onFileSelected) }
}2. 外部文件拖拽 (Drag & Drop)
在桌面端,用户经常从资源管理器直接拖拽文件到 App 中。
kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DropTargetBox() {
var dropText by remember { mutableStateOf("Drop files here") }
Box(
modifier = Modifier
.size(300.dp)
.background(Color.LightGray)
// 监听外部拖拽事件
.onExternalDrag(
onDragStart = { externalDragValue ->
// 可以检查拖进来的内容类型
dropText = "Dragging..."
},
onDragExit = {
dropText = "Drop files here"
},
onDrop = { externalDragValue ->
val dragData = externalDragValue.dragData
if (dragData is androidx.compose.ui.draganddrop.DragData.FilesList) {
// 获取文件路径列表
val files = dragData.readFiles()
dropText = "Dropped: ${files.joinToString { it.substringAfterLast("/") }}"
}
}
)
) {
Text(dropText, Modifier.align(Alignment.Center))
}
}3. 资源访问 (Resources)
从 Compose Multiplatform 1.6.0 开始,官方提供了全新的资源库 compose.components.resources。
目录结构
所有资源都放在 composeApp/src/commonMain/composeResources 目录下:
text
commonMain/
composeResources/
drawable/
logo.xml
image.png
font/
myfont.ttf
values/
strings.xml代码调用
会自动生成 Res 对象。
kotlin
import compose.components.resources.*
Image(
painter = painterResource(Res.drawable.image),
contentDescription = stringResource(Res.string.app_name)
)异步加载
CMP 的资源加载本质上是异步的(尤其在 Web 端)。虽然 painterResource 看起来是同步的,但它内部处理了挂起。
4. 平台判断
有时你需要在 commonMain 中根据平台渲染不同的 UI。
kotlin
// Platform.kt
expect val currentPlatform: Platform
enum class Platform { Android, IOS, Desktop, Web }
// AndroidMain
actual val currentPlatform = Platform.Android
// DesktopMain
actual val currentPlatform = Platform.Desktopkotlin
// UI 代码
if (currentPlatform == Platform.Desktop) {
// 显示鼠标特有的悬停提示
TooltipArea(...)
}