任务五 加载在线歌曲数据
任务四已经完成本地列表显示,现在接入在线接口:用 OkHttp 请求数据、Gson 解析响应,再把歌曲列表交给适配器;请求失败时清空列表并显示提示。
一、任务目标
完成后,应用将能够:
- 声明网络权限、允许明文 HTTP 访问,并配置 OkHttp、Gson 依赖。
- 异步请求歌曲接口,将 JSON 转换为
SongResponse。 - 成功时显示在线歌曲,失败时清空列表并显示提示。
二、配置网络访问
1. 声明网络权限
打开 app/src/main/AndroidManifest.xml,在 <manifest> 标签内、<application> 标签前加入 INTERNET 权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 申请网络访问权限 --> <uses-permission android:name="android.permission.INTERNET" />
<application ...> ... </application></manifest>Android 应用默认不能访问网络,声明权限后我们的音乐播放器才能请求在线歌曲。
2. 允许 HTTP 明文访问
歌曲接口使用 https,但服务器上部分音频、封面资源的地址是 http。Android 9 及以上默认禁止应用发起 http 明文请求,这类地址会被系统直接拒绝,错误信息中包含 CLEARTEXT。打开 AndroidManifest.xml,在 <application> 标签上添加 android:usesCleartextTraffic 属性。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:usesCleartextTraffic="true" ...> ... </application></manifest>该属性允许应用发起 http 明文请求。正式项目可以用网络安全配置(networkSecurityConfig)按域名逐个放开,本项目为简便起见全局开启。
3. 添加 OkHttp 和 Gson
打开 app/build.gradle.kts,在 dependencies 中加入两个依赖。
dependencies { // ... 省略已有依赖。 implementation("com.google.code.gson:gson:2.11.0") // JSON 解析库 implementation("com.squareup.okhttp3:okhttp:4.10.0") // HTTP 请求库}保存文件并等待 Gradle 同步。
三、封装歌曲接口请求
1. 创建 MusicApi 并配置客户端
MusicApi 负责请求歌曲列表,并返回歌曲数据或失败原因。歌曲列表页面调用它获取数据,不必直接处理网络请求和 JSON 解析。
在 app/src/main/java/com/sziit/musicplayer 下创建 data 包,再创建 MusicApi.kt。先声明接口地址和 OkHttpClient。
package com.sziit.musicplayer.data
import okhttp3.OkHttpClientimport java.util.concurrent.TimeUnit
object MusicApi { // 歌曲列表接口地址,见任务三的接口说明 private const val MUSIC_LIST_URL = "https://api.food1.sziit.top/music/list" // 共用一个客户端,设置连接和读取超时。 private val client = OkHttpClient.Builder() .connectTimeout(8, TimeUnit.SECONDS) .readTimeout(8, TimeUnit.SECONDS) .build()}8 秒连接和读取超时可以避免网络异常时长时间等待。
2. 发起异步请求
在 MusicApi 中添加 fetchSongs(),构建请求并处理连接失败的情况。收到响应后先关闭响应资源,下一小点再解析歌曲数据。
import android.util.Logimport com.sziit.musicplayer.model.Songimport okhttp3.Callimport okhttp3.Callbackimport okhttp3.Requestimport okhttp3.Responseimport java.io.IOException
object MusicApi { // ... 省略接口地址和 OkHttpClient。
// 请求歌曲列表;成功和失败分别回调给调用方 fun fetchSongs( onSuccess: (List<Song>) -> Unit, onFailure: (String) -> Unit ) { val request = Request.Builder().url(MUSIC_LIST_URL).get().build() // 构建 GET 请求 client.newCall(request).enqueue(object : Callback { // 排入队列,在后台线程异步执行 override fun onFailure(call: Call, e: IOException) { // 连不上服务器或网络中断时回调 Log.e("MusicApi", "request failed", e) onFailure(e.message ?: "网络请求失败") }
override fun onResponse(call: Call, response: Response) { response.close() // 关闭响应,暂不读取歌曲数据。 } }) }}enqueue() 在后台线程执行请求,不会阻塞应用主界面。请求失败时通过 onFailure 返回错误信息。
3. 解析服务器响应
在 fetchSongs() 的 onResponse() 中,将直接关闭响应的语句替换为数据解析逻辑。使用 response.use {} 读取响应,处理结束后会自动关闭资源。
import com.google.gson.Gsonimport com.sziit.musicplayer.model.SongResponse
object MusicApi { // ... 省略接口地址和 OkHttpClient。
fun fetchSongs( onSuccess: (List<Song>) -> Unit, onFailure: (String) -> Unit ) { // ... 省略请求构建。 client.newCall(request).enqueue(object : Callback { // 排入队列,在后台线程异步执行 // ... 省略 onFailure()。
override fun onResponse(call: Call, response: Response) { // 收到响应,仍在后台线程。 response.close() // 关闭响应,暂不读取歌曲数据。 response.use { // 花括号结束时自动关闭响应 if (!it.isSuccessful) { // HTTP 状态码不是 2xx onFailure("服务器返回 ${it.code}") return } try { // 把响应体的 JSON 文本转换成 SongResponse 对象 val result = Gson().fromJson( it.body?.string().orEmpty(), SongResponse::class.java ) if (result.code == 200 && result.data != null) { // 业务状态码和数据都有效才算成功 onSuccess(result.data) } else { onFailure(result.msg ?: "接口返回失败") } } catch (e: Exception) { onFailure("数据解析失败") } } } }) }}代码先检查 HTTP 状态,再用 Gson 解析响应。接口状态和歌曲列表都有效时调用 onSuccess,其余情况统一调用 onFailure。
四、在 Fragment 中加载在线数据
1. 请求并显示在线歌曲
在 onViewCreated() 末尾,将测试数据调用替换为 MusicApi.fetchSongs()。先完成成功回调,失败回调暂时保留为空。
import com.sziit.musicplayer.data.MusicApi
class MusicListFragment : Fragment() { // ... 省略已有字段和其他方法。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // ... 省略 RecyclerView 初始化代码。 adapter.setData(sampleSongs()) MusicApi.fetchSongs( onSuccess = { songs -> binding.root.post { adapter.setData(songs) } // 切换回主线程更新列表。 }, onFailure = { } ) }}OkHttp 回调运行在后台线程,root.post {} 将列表更新切换回主线程。
2. 处理请求失败结果
把 onViewCreated() 中 MusicApi.fetchSongs(...) 里的空 onFailure 替换为失败处理代码。
import android.widget.Toast
class MusicListFragment : Fragment() { // ... 省略已有字段和其他方法。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // ... 省略 RecyclerView 初始化代码。 MusicApi.fetchSongs( onSuccess = { songs -> // ... 保留已有的列表更新代码。 }, onFailure = { } onFailure = { message -> // 失败时清空列表,并用 Toast 显示原因。 binding.root.post { adapter.setData(emptyList()) Toast.makeText( requireContext(), "歌曲加载失败:$message", Toast.LENGTH_SHORT ).show() } } ) }}网络失败时列表清空,Toast 提示失败原因;页面保持可用,用户可以重新启动应用重试。
3. 删除原有测试数据
在线请求和失败处理都已完成,删除任务四中的 sampleSongs() 方法,以及不再使用的 Song 导入。
import com.sziit.musicplayer.model.Song
class MusicListFragment : Fragment() { // ... 省略已有字段和生命周期方法。
// 用于验证列表显示的测试数据。 private fun sampleSongs() = listOf( Song(name = "晴天", singer = "周杰伦"), Song(name = "起风了", singer = "买辣椒也用券"), Song(name = "海阔天空", singer = "Beyond") )}删除测试数据后,正常联网时只显示接口返回的歌曲。
五、运行验证
启动应用并等待请求完成,列表显示接口返回的歌曲名称和歌手;具有 mvPath 的歌曲显示视频图标。
运行到设备,打开应用并等待列表加载,检查:
- 列表内容是否由接口返回,而不是任务四的三首固定测试歌曲。
- 歌曲名称和歌手是否正确显示。
- 有
mvPath的歌曲是否显示视频图标。 - 关闭网络后重新启动,列表是否为空并弹出失败提示。
如果列表一直为空,检查设备网络、INTERNET 权限和接口地址;如果出现解析错误,检查 SongResponse 字段与接口 JSON 是否一致。
任务小结
歌曲列表已经从本地测试数据切换为在线接口数据。OkHttp 负责请求,Gson 完成解析,Fragment 在主线程更新 RecyclerView;网络失败时显示空列表并给出提示。