
循序渐进Kotlin+Compose+Multiplatform跨平台之桌面端实现系列:
完整案例:Kotlin+Compose+Multiplatform跨平台之桌面端实现(一)
完整案例:Kotlin+Compose+Multiplatform跨平台之桌面端实现(二)
效果图如下
:

一、前言
在前面两篇文章已经介绍了项目工程,打包配置,基础相关使用,
今天来到Kotlin+Compose+Multiplatform
之桌面端
- 数据库使用,
- 音乐播放器,实现
- 音频动画效果
- LRC歌词展示
下面进行详细分解
二、数据库使用
在Kotlin+Compose+Multiplatform
之桌面端上面有很多数据库可以使用, 本案例采用介绍的是:
scss
// Kotlinx.Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// SQLite JDBC 驱动
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
- 此方案
- 可以通过Kotlin序列化可直接将数据库查询结果映射为数据类
- 可以在编译器插件生成序列化代码,避免运行时反射开销
- 它支持跨平台数据格式转换(如SQLite结果→JSON→UI模型)
SQLite+ JDBC
的组合实现轻量级持久化,嵌入式数据库无需额外服务部署,特别适合桌面端- 通过
JDBC
标准接口实现ACID
事务,兼容所有Kotlin/JVM平台
(Mac,Window,linux
) - 它与
Compose
状态管理无缝集成,支持协程异步操作
- 接入配置:
- 在
build.gradle
中添加如下配置:
arduino
plugins {
kotlin("plugin.serialization") version "1.9.0" // Kotlin 序列化插件
}
- 在
jvmMain.dependencies {}
加依赖引入模块:
scss
jvmMain.dependencies {
// Kotlinx.Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// SQLite JDBC 驱动
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
}
- 具体使用:
- 创建数据库获取到连接
connection
:
kotlin
private val dbName = "music.db"
private val connection: Connection by lazy {
val dbPath = DatabaseUtils.getDatabasePath(dbName)
DriverManager.getConnection("jdbc:sqlite:$dbPath")
}
- 创建表结构:通
connection.createStatement().executeUpdate
直接可以执行创建表的sql语句
python
private fun createTable() {
connection.createStatement().executeUpdate(
"""
CREATE TABLE IF NOT EXISTS play_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
musicID TEXT NOT NULL,
name TEXT NOT NULL,
singer TEXT NOT NULL,
pic TEXT NOT NULL,
url TEXT NOT NULL,
lrc TEXT NOT NULL,
musicSuffer TEXT NOT NULL,
localFile INTEGER CHECK (localFile IN (0, 1))
)
"""
)
}
- 插入一条数据如下:(
注意:
?的顺序是从1开始依次往后连续加上去的
)
vbscript
//插入播放列表
fun inserPlayItem(user: MusicItem): Int {
val sql = "INSERT INTO play_list (musicID, name, singer, pic, url, lrc, musicSuffer,localFile) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
statement.setString(1, user.musicID)
statement.setString(2, user.name)
statement.setString(3, user.singer)
statement.setString(4, user.pic)
statement.setString(5, user.url)
statement.setString(6, user.lrc)
statement.setString(7, user.musicSuffer)
statement.setBoolean(8, user.localFile)
statement.executeUpdate()
return statement.generatedKeys.getInt(1) // 返回生成的 ID
}
- 从表中查询数据转化成实体列表:
vbscript
//查询播放列表
fun getAllPlayList(): List<MusicItem> {
val list = mutableListOf<MusicItem>()
val resultSet = connection.createStatement().executeQuery("SELECT * FROM play_list ORDER BY id ASC")
while (resultSet.next()) {
list.add(
MusicItem(
resultSet.getString("musicID"),
resultSet.getString("name"),
resultSet.getString("singer"),
resultSet.getString("pic"),
resultSet.getString("url"),
resultSet.getString("lrc"),
resultSet.getString("musicSuffer"),
resultSet.getBoolean("localFile"),
resultSet.getInt("id")
)
)
}
return list
}
- 操作数据库相关删除:
kotlin
//删除播放列表,删除表结构
fun dropTable(): Int {
val statement = connection.createStatement()
// 执行删除表操作
statement.executeUpdate("DROP TABLE IF EXISTS play_list")
statement.close()
return 1
}
//删除整个播放列表中数据
fun delList(): Int {
val statement = connection.createStatement()
// 执行清空操作
statement.executeUpdate("DELETE FROM play_list")
statement.close()
return 1
}
//通过musicID 删除指定数据
fun delItemByID(musicID: String): Int {
val statement = connection.prepareStatement(
"DELETE FROM play_list WHERE musicID = ?"
).apply {
setString(1, musicID)
}
val deletedRows = statement.executeUpdate()
statement.close()
return 1
}
- 对数据库中某条数据进行修改如下:
kotlin
// 更新指定的字段值
fun updateField(musicID: String): Int {
val sql = "UPDATE play_list SET localFile = ? WHERE musicID = ?"
val statement = connection.prepareStatement(sql).apply {
setInt(1, 1)
setString(2, musicID)
executeUpdate()
}
statement.close()
return 1
}
三、音乐播放器
在Kotlin+Compose+Multiplatform
之桌面端上面有比较流行的方案:ComposeMultiplatformMediaPlaye
r方案,它是采用 采用跨平台媒体播放库,支持Windows/macOS/Linux
三端统一开发,核心集成VLC引擎实现硬件加速解码,但是它要电脑安装好VLC环境配置,比如:Windows要安装
- VLC官方播放器
,
所以本案例不采用它,我们采用它
而是采用,电脑无需额外安装任何配置的,JavaFX提供开箱即用的Media
/MediaPlaye
即:org.openjfx:javafx
,
- 它具备以下优势:
- 原生支持
MP3/WAV/AAC
等主流音频格式的解码播放,无需额外依赖第三方解码库 - 通过
DirectX(Windows)/CoreAudio(macOS)
等原生图形接口实现硬件级音频加速,显著降低CPU占用率 - 支持一套代码无缝运行于
Windows/macOS/Linux
,自动适配各平台音频驱动(如Windows WASAPI、Linux ALSA
)开发者无需处理平台差异逻辑 - 它从
JDK11
起采用模块化设计,通过javafx-media
模块精准控制依赖,应用打包体积比传统方案减少40%+
- 接入相关配置:
- 在
build.gradle
中添加如下配置:
ini
plugins {
id("org.openjfx.javafxplugin") version "0.1.0"
}
javafx {
version = "20.0.2"
modules = listOf("javafx.media", "javafx.graphics", "javafx-swing")
}
- 在
jvmMain.dependencies {}
内添加实现播放器的相关依赖,如下:
javascript
jvmMain.dependencies {
val javafxPlatform = System.getProperty("os.name").lowercase().let {
when {
it.contains("linux") -> "linux"
it.contains("mac") -> "mac"
else -> "win"
}
}
implementation("org.openjfx:javafx-base:20.0.2:$javafxPlatform") {
exclude(group = "org.openjfx", module = "javafx-base")
} // 必须的基础模块:ml-citation{ref="7" data="citationList"}
implementation("org.openjfx:javafx-media:20.0.2:$javafxPlatform") {
exclude(group = "org.openjfx", module = "javafx-media")
}
implementation("org.openjfx:javafx-graphics:20.0.2:$javafxPlatform") {
exclude(group = "org.openjfx", module = "javafx-graphics")
}
implementation("org.openjfx:javafx-swing:20.0.2:$javafxPlatform") {
exclude(group = "org.openjfx", module = "javafx-swing")
}
}
- 使用步骤和相关API:
- 提前初始化
Platform.startup { }
- 播放加载音乐如下示代码:需要注意的是必须在主线程调用该方法:即,下面的代码必须包在
Platform.runLater{ }
内部或者SwingUtilities.invokeLater { }
的内部,否则会抛出IllegalStateException
scss
val media = Media(cacheFile.toURI().toString())
mediaPlayer = MediaPlayer(media).apply {
setOnReady {
play()
}
}
- 音乐
播放暂停,停止,是否正在播放
如下操作:
kotlin
//提供是否正在播放
fun isPlaying() = mediaPlayer?.status == MediaPlayer.Status.PLAYING
//提供暂停播放方法
fun pause() = mediaPlayer?.pause()
//提供停止播放方法
fun stop() = mediaPlayer?.stop()
- 播放
前一首,下一首
:主要是通过 播放列表的当前索引切换:position + 1
后获得的播放资源重新播放即播放下一首,position - 1
后获得的播放资源重新播放即播放上一首。 5. 获取播放的当前时间:val time = mediaPlayer?.currentTime?.toSeconds()?.toLong() ?: 0L
6. 获取播放总时长:` fun totalDuration(): Long = mediaPlayer?.totalDuration?.toSeconds()?.toLong() ?: 0L 7. 根据当前时间和总时长,可以计算出播放进度。 8. 拖动UI进度条seekTo,需要计算出拖动后的时间然后直接调用:mediaPlayer?.seek(Duration.seconds(seconds))
9. 播放文件缓存配置:需要手动设置
less
val cacheFile = if (!localFile) {
File(PlatformKVStore.getDownloadDir(), downloadFileName).apply {
DownLoadUtils.instance.WXDownload2(playItem.url, this@apply)
}
} else {
println("zou huan cun ")
File(PlatformKVStore.getDownloadDir(), downloadFileName)
}
val media = Media(cacheFile.toURI().toString())
四、音频动画效果
本案例播放器音频动画效果:主要包含2个:
- 专辑封面转圈图片,
- 从播放器那边拿到的实时动态数据绘制条形变化,和转圈外围颜色条纹:
- 从播放器里面拿到音频数据:
- 设置获取音频数据配置:在JavaFX媒体框架中,
audioSpectrumInterval
、audioSpectrumNumBands
和audioSpectrumThreshold
是用于控制音频频谱分析的核心参数,其中:audioSpectrumInterval
控制频谱数据更新的时间间隔(秒),默认值为0.1秒。较短的间隔可实现更实时的频谱反馈,但会增加CPU负载。audioSpectrumNumBands
指定频谱分析的频段数量,默认128个频段。增加频段数可提高频谱分辨率,但会提升计算复杂度。audioSpectrumThreshold
设置频谱灵敏度阈值(dB),默认-60dB。低于该阈值的音频信号将被过滤,避免显示噪声干扰。
本案例配置如下:
ini
MediaPlayer(media).apply {
audioSpectrumInterval = 0.03
audioSpectrumNumBands = 320
audioSpectrumThreshold = -60
//监听频谱数据
audioSpectrumListener = AudioSpectrumListener { _, _, magnitudes, phases ->
//magnitudes 就是频谱数据,FloatArray类型
}
- Compose UI 部分,通过
.graphicsLayer { rotationZ}
修改rotationZ
的值来选择封面图片
ini
AsyncImage(
model = picUrl, contentDescription = "唱片 Network Image", modifier = Modifier.padding(20.dp, 0.dp, 0.dp, 80.dp).size(300.dp).clip(CircleShape).border(
width = 45.dp, color = Color.Black, shape = CircleShape
).padding(45.dp).graphicsLayer {
rotationZ = viewModel.sheetDiskRotate.value
}, contentScale = ContentScale.Crop
)
- 通过
Compose
的Canvas
来绘频谱数据,这个没什么难度。如下:条形频谱和圆圈周围动态频谱
scss
//条形动态频谱
@Composable
fun AudioVisualizer(viewModel: PlayerViewModel) {
var canvasWidth by remember { mutableFloatStateOf(0f) }
val spectrumData by viewModel.spectrumDataFlow.collectAsState()
Canvas(modifier = Modifier.padding(10.dp, 430.dp, 0.dp, 0.dp).width(300.dp).onSizeChanged { canvasWidth = it.width.toFloat() }) {
if (spectrumData.isNotEmpty()) {
drawRect(Color.Transparent)
val barWidth = canvasWidth / spectrumData.size
spectrumData.forEachIndexed { i, value ->
// val newValue = value //if (value == -60.0f) randomInRange(-60f, -45f) else value
val newValue = if (value == -60.0f) randomInRange(-60f, -45f) else value
val height = (newValue + 60) * 3f // 标准化幅度值
val newIndex = i //if (i > haftSize) (i + haftSize) % dataSize else i + haftSize
drawRect(
color = Color.hsv(newIndex * 360f / spectrumData.size, 1f, 1f), topLeft = Offset(newIndex * barWidth, size.height - height), size = Size(barWidth * 0.8f, height)
)
}
}
}
}
//圆圈周围动态频谱
@Composable
fun CircularAudioVisualizer(viewModel: PlayerViewModel) {
var center by remember { mutableStateOf(Offset.Zero) }
var baseRadius by remember { mutableStateOf(0f) }
val spectrumData by viewModel.spectrumDataFlow.collectAsState()
Canvas(modifier = Modifier.padding(20.dp, 0.dp, 0.dp, 80.dp).size(280.dp).onSizeChanged {
center = Offset(it.width.toFloat() / 2, it.height.toFloat() / 2)
baseRadius = min(it.width, it.height) * 0.55f
}) {
if (spectrumData.isNotEmpty()) {
drawCircle(Color.Transparent, baseRadius, center, style = Stroke(2f))
val angleStep = 2f * PI / spectrumData.size
spectrumData.forEachIndexed { i, value ->
val newValue = value //if (value == -60.0f) randomInRange(-60f, -46f) else value
val normalized = newValue * 2 / 60f
val spikeLength = baseRadius * 0.08f * normalized
val angle = i * angleStep
val startX = center.x + baseRadius * cos(angle).toFloat()
val startY = center.y + baseRadius * sin(angle).toFloat()
val endX = startX + spikeLength * cos(angle).toFloat()
val endY = startY + spikeLength * sin(angle).toFloat()
drawLine(
color = Color.hsv(i * 360f / spectrumData.size, 1f, 1f), start = Offset(startX, startY), end = Offset(endX, endY), strokeWidth = 6f
)
drawCircle(
color = Color.hsv(i * 360f / spectrumData.size, 1f, 1f), radius = 4f, center = Offset(endX, endY)
)
}
}
}
}
五、LRC歌词展示
LRC歌词展示中:这个没什么难度,使用Compose 的 LazyColumn
展示歌词的每一行,获取到播放器当前播放时间,与解析LRC歌词头部时间进行对比,返回 LazyColumn
中列表的位置索引,然后根据索引对当前歌词行文字 颜色,大小进行设置,详细请看项目地址源码:
六、总结
本篇是完整案例:Kotlin + Compose + Multiplatform
之桌面端实现的第三篇,
主要介绍了:
- 数据库使用,
- 音乐播放器,实现
- 音频动画效果
- LRC歌词展示
相信会给你带来一定帮助。
后续还有:
- 视频播放介绍
- 桌面端插件化
- 跨端到
移动端,桌面端,web端
的公用研究。