任务五 加载在线歌曲数据

任务四已经完成本地列表显示,现在接入在线接口:用 OkHttp 请求数据、Gson 解析响应,再把歌曲列表交给适配器;请求失败时清空列表并显示提示。

一、任务目标

完成后,应用将能够:

  • 声明网络权限、允许明文 HTTP 访问,并配置 OkHttp、Gson 依赖。
  • 异步请求歌曲接口,将 JSON 转换为 SongResponse。
  • 成功时显示在线歌曲,失败时清空列表并显示提示。

二、配置网络访问

1. 声明网络权限

打开 app/src/main/AndroidManifest.xml,在 <manifest> 标签内、<application> 标签前加入 INTERNET 权限。

app/src/main/AndroidManifest.xml
<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 属性。

app/src/main/AndroidManifest.xml
<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 中加入两个依赖。

app/build.gradle.kts
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。

app/src/main/java/com/sziit/musicplayer/data/MusicApi.kt
package com.sziit.musicplayer.data
import okhttp3.OkHttpClient
import 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(),构建请求并处理连接失败的情况。收到响应后先关闭响应资源,下一小点再解析歌曲数据。

app/src/main/java/com/sziit/musicplayer/data/MusicApi.kt
import android.util.Log
import com.sziit.musicplayer.model.Song
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import 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 {} 读取响应,处理结束后会自动关闭资源。

app/src/main/java/com/sziit/musicplayer/data/MusicApi.kt
import com.google.gson.Gson
import 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()。先完成成功回调,失败回调暂时保留为空。

app/src/main/java/com/sziit/musicplayer/ui/fragment/MusicListFragment.kt
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 替换为失败处理代码。

app/src/main/java/com/sziit/musicplayer/ui/fragment/MusicListFragment.kt
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 导入。

app/src/main/java/com/sziit/musicplayer/ui/fragment/MusicListFragment.kt
import com.sziit.musicplayer.model.Song
class MusicListFragment : Fragment() {
// ... 省略已有字段和生命周期方法。
// 用于验证列表显示的测试数据。
private fun sampleSongs() = listOf(
Song(name = "晴天", singer = "周杰伦"),
Song(name = "起风了", singer = "买辣椒也用券"),
Song(name = "海阔天空", singer = "Beyond")
)
}

删除测试数据后,正常联网时只显示接口返回的歌曲。

五、运行验证

启动应用并等待请求完成,列表显示接口返回的歌曲名称和歌手;具有 mvPath 的歌曲显示视频图标。

在线歌曲列表运行效果

运行到设备,打开应用并等待列表加载,检查:

  1. 列表内容是否由接口返回,而不是任务四的三首固定测试歌曲。
  2. 歌曲名称和歌手是否正确显示。
  3. 有 mvPath 的歌曲是否显示视频图标。
  4. 关闭网络后重新启动,列表是否为空并弹出失败提示。

如果列表一直为空,检查设备网络、INTERNET 权限和接口地址;如果出现解析错误,检查 SongResponse 字段与接口 JSON 是否一致。

任务小结

歌曲列表已经从本地测试数据切换为在线接口数据。OkHttp 负责请求,Gson 完成解析,Fragment 在主线程更新 RecyclerView;网络失败时显示空列表并给出提示。