安卓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 数据库实现播放列表的持久化存储



相关文章
|
3月前
|
存储 消息中间件 人工智能
【04】AI辅助编程完整的安卓二次商业实战-寻找修改替换新UI首页图标-菜单图标-消息列表图标-优雅草伊凡
【04】AI辅助编程完整的安卓二次商业实战-寻找修改替换新UI首页图标-菜单图标-消息列表图标-优雅草伊凡
185 4
|
7月前
|
缓存 Android开发 iOS开发
Kotlin跨平台Compose Multiplatform实战指南
Kotlin Multiplatform (KMP) 结合 Compose Multiplatform,助力开发者用一套代码构建跨平台应用(Android、iOS、桌面和 Web)。本文提供实战指南,涵盖环境搭建、项目结构、共享 UI 编写、平台适配、状态管理及资源处理等内容。通过 expect/actual 处理差异,借助官方文档与示例项目学习,减少重复代码,优化多平台开发体验。
1748 18
|
6月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
300 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
10月前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
345 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
10月前
|
Android开发 开发者 Kotlin
Android实战经验之Kotlin中快速实现MVI架构
MVI架构通过单向数据流和不可变状态,提供了一种清晰、可预测的状态管理方式。在Kotlin中实现MVI架构,不仅提高了代码的可维护性和可测试性,还能更好地应对复杂的UI交互和状态管理。通过本文的介绍,希望开发者能够掌握MVI架构的核心思想,并在实际项目中灵活应用。
483 8
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
347 94
|
11月前
|
编译器 Android开发 开发者
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
Lambda表达式和匿名函数都是Kotlin中强大的特性,帮助开发者编写简洁而高效的代码。理解它们的区别和适用场景,有助于选择最合适的方式来解决问题。希望本文的详细讲解和示例能够帮助你在Kotlin开发中更好地运用这些特性。
298 9
|
12月前
|
存储 监控 API
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
app开发之安卓Android+苹果ios打包所有权限对应解释列表【长期更新】-以及默认打包自动添加权限列表和简化后的基本打包权限列表以uniapp为例-优雅草央千澈
1239 11
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
291 1
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造