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



相关文章
|
1月前
|
ARouter Android开发
Android不同module布局文件重名被覆盖
Android不同module布局文件重名被覆盖
|
3月前
|
Java Android开发 C++
Android Studio JNI 使用模板:c/cpp源文件的集成编译,快速上手
本文提供了一个Android Studio中JNI使用的模板,包括创建C/C++源文件、编辑CMakeLists.txt、编写JNI接口代码、配置build.gradle以及编译生成.so库的详细步骤,以帮助开发者快速上手Android平台的JNI开发和编译过程。
238 1
|
30天前
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
2月前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
20 1
|
1月前
|
ARouter Android开发
Android不同module布局文件重名被覆盖
Android不同module布局文件重名被覆盖
109 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的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
62 4
|
3月前
|
开发工具 git 索引
repo sync 更新源码 android-12.0.0_r34, fatal: 不能重置索引文件至版本 ‘v2.27^0‘。
本文描述了在更新AOSP 12源码时遇到的repo同步错误,并提供了通过手动git pull更新repo工具来解决这一问题的方法。
123 1
|
2月前
|
调度 Android开发 开发者
探索安卓开发中的新技术:Kotlin协程
【9月更文挑战第9天】本文将深入探讨Kotlin协程在安卓开发中的应用,揭示其如何优雅地处理异步任务。我们将从基础概念入手,逐步深入到实际开发场景,最后通过代码示例直观展示协程的魔力。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往更高效、更简洁代码的大门。
|
3月前
|
移动开发 Java Android开发
探索安卓应用开发的新趋势:Kotlin与Coroutines
【8月更文挑战第31天】本文旨在为读者揭示安卓开发中的最新潮流,特别是Kotlin语言及其协程特性如何重塑移动应用的构建方式。我们将通过实际代码示例深入理解Kotlin简洁语法背后的强大功能,并探讨如何利用Coroutines优雅地处理异步任务,从而提升应用性能和用户体验。无论你是安卓开发的新手还是资深开发者,这篇文章都将为你带来新的启示和灵感。