安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐

简介: 为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤:1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。2. **创建UI按钮**:在界面中创建添加和删除按钮。3. **数据库功能**:使用Room数据库来存储音频文件信息。4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。

需求描述:

安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐



为了在 UI 层添加按钮来添加和删除本地音乐文件,首先需要实现几个额外的功能:

  1. 将用户选择本地音乐的功能集成到应用中。
  2. 在 UI 层创建按钮,允许用户选择添加音乐文件到播放列表。
  3. 提供功能来删除播放列表中的音乐文件。
  4. 集成这些功能到 ViewModel 和 UI 层。


依赖项


dependencies {
    // Jetpack Compose
    implementation "androidx.compose.ui:ui:1.1.1"
    implementation "androidx.compose.material:material:1.1.1"
    implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
    implementation "androidx.activity:activity-compose:1.4.0"
    implementation "androidx.activity:activity-ktx:1.4.0"

    // ExoPlayer
    implementation 'com.google.android.exoplayer:exoplayer:2.14.1'

    // Room for local database
    implementation "androidx.room:room-runtime:2.4.2"
    kapt "androidx.room:room-compiler:2.4.2"
    implementation "androidx.room:room-ktx:2.4.2"
}




数据库和数据实体

如果还没有这样定义,创建一个数据实体类和 Room 数据库来持久化音频文件:


import androidx.room.*

@Entity(tableName = "audio_files")
data class AudioFile(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "file_name") val fileName: String,
    @ColumnInfo(name = "file_uri") val fileUri: String
)

@Dao
interface AudioFileDao {
    @Query("SELECT * FROM audio_files")
    fun getAll(): List<AudioFile>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(audioFile: AudioFile)

    @Delete
    fun delete(audioFile: AudioFile)
}

@Database(entities = [AudioFile::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun audioFileDao(): AudioFileDao
}


更新 ViewModel

更新你的 ViewModel 来添加和删除音频文件:



import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.Room
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class AudioPlayerViewModel(application: Application) : AndroidViewModel(application) {
    private val context: Context = application.applicationContext
    private val exoPlayer: ExoPlayer = ExoPlayer.Builder(application.applicationContext).build()
    private val db: AppDatabase = Room.databaseBuilder(
        application.applicationContext,
        AppDatabase::class.java, "audio_db"
    ).build()

    private val audioFileDao = db.audioFileDao()
    private val _audioFiles = mutableListOf<AudioFile>()
    val audioFiles: List<AudioFile>
        get() = _audioFiles

    init {
        loadAudioFiles()
    }

    private fun loadAudioFiles() {
        viewModelScope.launch(Dispatchers.IO) {
            val audioFilesFromDb = audioFileDao.getAll()
            _audioFiles.clear()
            _audioFiles.addAll(audioFilesFromDb)
        }
    }

    fun addAudioFile(fileName: String, fileUri: String) {
        viewModelScope.launch(Dispatchers.IO) {
            val audioFile = AudioFile(fileName = fileName, fileUri = fileUri)
            audioFileDao.insert(audioFile)
            _audioFiles.add(audioFile)
        }
    }

    fun deleteAudioFile(audioFile: AudioFile) {
        viewModelScope.launch(Dispatchers.IO) {
            audioFileDao.delete(audioFile)
            _audioFiles.remove(audioFile)
        }
    }

    fun playAudio(audioFile: AudioFile) {
        val uri = Uri.parse(audioFile.fileUri)
        val mediaItem = MediaItem.fromUri(uri)
        exoPlayer.setMediaItem(mediaItem)
        exoPlayer.prepare()
        exoPlayer.playWhenReady = true
    }

    override fun onCleared() {
        super.onCleared()
        exoPlayer.release()
    }
}



UI层


支持添加、删除音乐, 以及播放功能


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp(audioPlayerViewModel: AudioPlayerViewModel = viewModel()) {
    var newFileName by remember { mutableStateOf("") }
    val audioFiles by rememberUpdatedState(audioPlayerViewModel.audioFiles)

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent(),
        onResult = { uri ->
            uri?.let {
                audioPlayerViewModel.addAudioFile(newFileName, it.toString())
                newFileName = ""
            }
        }
    )

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Audio Player") })
        },
        content = {
            Column(
                Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            ) {
                OutlinedTextField(
                    value = newFileName,
                    onValueChange = { newFileName = it },
                    label = { Text("New Audio File Name") }
                )
                Spacer(modifier = Modifier.height(8.dp))
                Button(
                    onClick = {
                        if (newFileName.isNotEmpty()) {
                            launcher.launch("audio/*")
                        }
                    },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("Add to Playlist")
                }
                Spacer(modifier = Modifier.height(16.dp))
                LazyColumn {
                    items(audioFiles) { audioFile ->
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(8.dp),
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            Column {
                                Text(
                                    text = "File: ${audioFile.fileName}",
                                    fontSize = 20.sp
                                )
                                Text(
                                    text = "URI: ${audioFile.fileUri}",
                                    fontSize = 12.sp
                                )
                            }
                            Row {
                                IconButton(onClick = { audioPlayerViewModel.playAudio(audioFile) }) {
                                    Icon(
                                        imageVector = Icons.Default.PlayArrow,
                                        contentDescription = "Play"
                                    )
                                }
                                IconButton(onClick = { audioPlayerViewModel.deleteAudioFile(audioFile) }) {
                                    Icon(
                                        imageVector = Icons.Default.Delete,
                                        contentDescription = "Delete"
                                    )
                                }
                            }
                        }
                    }
                }
                Spacer(modifier = Modifier.height(16.dp))
                Button(
                    onClick = {
                        if (audioFiles.isNotEmpty()) {
                            audioPlayerViewModel.playAudio(audioFiles.random())
                        }
                    },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("Play Random Audio")
                }
            }
        }
    )
}



解释

  1. 依赖项:
  • 在 build.gradle 文件中包括 ExoPlayer 和 Room 相关的依赖项。
  1. 数据库和数据实体:
  • 创建 AudioFile 数据实体类,并包括 id、fileName 和 fileUri 字段。
  • 在 AppDatabase 中包含 AudioFileDao,并实现基本的增删查操作。
  1. ViewModel:
  • AudioPlayerViewModel 类管理每个 ExoPlayer 实例,并实现播放逻辑。
  • addAudioFile 和 deleteAudioFile 方法分别用于添加和删除播放列表中的音频文件。
  • playAudio 方法根据音频文件名创建媒体项,然后通过 ExoPlayer 播放。
  • loadAudioFiles 方法从数据库加载持久化的播放列表。
  1. UI 层:
  • OutlinedTextField 用于输入新的音频文件名。
  • Button 控件用于打开内容选择器,让用户选择本地音频文件。
  • 使用 LazyColumn 显示播放列表。
  • 对于每个播放列表项,提供播放和删除按钮。
  • 添加一个按钮来随机播放列表中的音频文件。

这样,在 UI 层,可以通过按钮选择本地音频文件并将其添加到播放列表,或者从播放列表中删除。ExoPlayer 的逻辑集中在 ViewModel 中,与 UI 层完全解耦,同时通过 Room 数据库实现播放列表的持久化存储



相关文章
|
2月前
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
185 93
|
21天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
25 1
|
27天前
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
29天前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
42 4
|
30天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
19 1
|
2月前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
19 1
|
1月前
|
Android开发 Kotlin
Android面试题之Kotlin中如何实现串行和并行任务?
本文介绍了 Kotlin 中 `async` 和 `await` 在并发编程中的应用,包括并行与串行任务的处理方法。并通过示例代码展示了如何启动并收集异步任务的结果。
22 0
|
1月前
|
Java 调度 Android开发
Android面试题之Kotlin中async 和 await实现并发的原理和面试总结
本文首发于公众号“AntDream”,详细解析了Kotlin协程中`async`与`await`的原理及其非阻塞特性,并提供了相关面试题及答案。协程作为轻量级线程,由Kotlin运行时库管理,`async`用于启动协程并返回`Deferred`对象,`await`则用于等待该对象完成并获取结果。文章还探讨了协程与传统线程的区别,并展示了如何取消协程任务及正确释放资源。
23 0
|
2月前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
73 6
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
58 4

热门文章

最新文章