任务一 创建并绑定音乐播放服务
歌曲列表和播放栏已经能够显示。接下来创建 MusicService,由 MainActivity 启动,再让两个 Fragment 绑定服务并取得服务实例。
一、任务目标
完成后,应用将具备以下能力:
- 使用 ViewBinding 加载
PlayerFragment的布局。 - 创建并注册
MusicService,再由MainActivity启动服务。 PlayerFragment和MusicListFragment分别绑定同一个服务,并通过 Binder 获取服务实例。- 按 Fragment 视图生命周期完成服务绑定与解绑,并通过 Logcat 验证服务启动和连接结果。
二、创建并注册 MusicService
MusicService 是应用中统一管理播放功能的组件。歌曲列表和底部播放栏都需要控制歌曲播放;把播放状态和控制逻辑集中在 MusicService 中,两个 Fragment 就能操作同一个播放器,不必各自创建播放器或保存播放状态。
两个 Fragment 分别绑定 MusicService,并通过 Binder 获取服务实例,关系如下:
1. 创建服务类和 Binder
在 app/src/main/java/com/sziit/musicplayer 下创建 service 包,再创建 MusicService.kt。
package com.sziit.musicplayer.service
import android.app.Serviceimport android.content.Intentimport android.os.Binderimport android.os.IBinderimport android.util.Log
class MusicService : Service() { private companion object { const val TAG = "MusicLifecycle" }
// Binder 用于把当前 MusicService 实例提供给绑定它的组件。 private val binder = MusicBinder()
override fun onCreate() { super.onCreate() Log.i(TAG, "MusicService: 服务已创建") }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.i(TAG, "MusicService: 收到启动请求") return super.onStartCommand(intent, flags, startId) }
inner class MusicBinder : Binder() { // 返回外部 MusicService 实例,供 Fragment 调用服务方法。 fun getService(): MusicService = this@MusicService }
// 收到绑定请求时记录日志,并返回 Binder。 override fun onBind(intent: Intent): IBinder { Log.i(TAG, "MusicService: 收到绑定请求") return binder }}MusicService 继承 Android Service。onCreate() 和 onStartCommand() 分别记录服务创建和启动请求;MusicBinder 提供当前服务实例,onBind() 在收到绑定请求时返回 Binder。
2. 在 Manifest 中注册
创建服务类后,还要在 Manifest 中注册,系统才能识别并启动它。打开 app/src/main/AndroidManifest.xml,在 <application> 标签中加入 Service 声明。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application ...> ... <service android:name=".service.MusicService" android:exported="false" /> </application></manifest>android:exported="false" 表示该服务只供当前应用使用,其他应用不能通过组件名直接启动它。
三、启动并验证服务
1. 从 MainActivity 启动服务
打开 app/src/main/java/com/sziit/musicplayer/ui/activity/MainActivity.kt,导入 Intent 和 MusicService,并在 onCreate() 的 setContentView(binding.root) 之后启动服务。
package com.sziit.musicplayer.ui.activity
import android.content.Intent// ... 省略其他已有导入。import com.sziit.musicplayer.service.MusicService
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // ... 省略 onCreate() 中已有的页面初始化代码。 setContentView(binding.root) startService(Intent(this, MusicService::class.java)) }}startService() 使服务不只依赖 Fragment 的绑定关系存活。Activity 负责启动服务,播放器的具体操作由服务统一管理;Fragment 在需要时通过绑定获取实例。
2. 验证服务启动
运行应用后,在 Android Studio 中打开 Logcat,选择运行应用的设备,并按 MusicLifecycle 筛选日志。
截图中的“服务已创建”和“收到启动请求”表明 MainActivity 已成功启动 MusicService。
接下来分别在两个 Fragment 中绑定服务。它们会通过 Binder 取得同一个 MusicService 实例。
四、为 PlayerFragment 绑定服务
1. 完善 PlayerFragment 结构
打开 app/src/main/java/com/sziit/musicplayer/ui/fragment/PlayerFragment.kt,使用 ViewBinding 加载播放器界面布局,并返回其根视图。
package com.sziit.musicplayer.ui.fragment
import android.os.Bundleimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.fragment.app.Fragmentimport com.sziit.musicplayer.Rimport com.sziit.musicplayer.databinding.FragmentPlayerBinding
class PlayerFragment : Fragment(R.layout.fragment_player)
class PlayerFragment : Fragment() { // 在 onCreateView() 中初始化绑定对象后即可直接使用。 private lateinit var binding: FragmentPlayerBinding
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // 创建布局对应的 ViewBinding,并返回其根视图。 binding = FragmentPlayerBinding.inflate(inflater, container, false) return binding.root }
}lateinit 用于声明稍后初始化的非空属性,binding 会在 onCreateView() 中赋值。
保存后运行应用,页面显示应与转换前完全一致,说明布局加载方式替换成功。
2. 添加服务连接对象
在 PlayerFragment.kt 中添加服务连接对象,使 Fragment 在连接成功后取得 MusicService 实例。
package com.sziit.musicplayer.ui.fragment
import android.content.ComponentNameimport android.content.Contextimport android.content.Intentimport android.content.ServiceConnectionimport android.os.IBinderimport android.util.Log// ... 省略已有导入。import com.sziit.musicplayer.service.MusicService
class PlayerFragment : Fragment() { // ... 省略已有的 binding 字段和 onCreateView()。
private companion object { const val TAG = "MusicLifecycle" }
private var musicService: MusicService? = null
// bindService() 完成连接后会回调此对象。 private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { // 从 Binder 取回服务实例,供当前 Fragment 使用。 musicService = (service as MusicService.MusicBinder).getService() Log.i(TAG, "PlayerFragment: 已连接 MusicService") }
override fun onServiceDisconnected(name: ComponentName) { // 服务意外断开时清除引用,避免继续访问。 musicService = null } }
}onServiceConnected() 只保存服务实例,暂时不调用播放方法。服务断开时将引用清空,避免页面继续使用失效对象。
3. 执行绑定和解除绑定
在类中增加绑定状态字段,并在类的末尾添加绑定与解绑方法。
class PlayerFragment : Fragment() { // ... 省略 binding、musicService 和 serviceConnection 字段。 private var isServiceBound = false // ... 省略已有的 onCreateView()。
private fun bindMusicService() { val intent = Intent(requireActivity(), MusicService::class.java) // 若服务尚未运行,BIND_AUTO_CREATE 会先创建它。 requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) // 记录绑定状态,避免重复解绑。 isServiceBound = true }
private fun unbindMusicService() { if (isServiceBound) { requireActivity().unbindService(serviceConnection) isServiceBound = false } // 解绑后不再保留服务引用。 musicService = null }}BIND_AUTO_CREATE 表示服务尚未创建时自动创建服务。解除绑定前检查 isServiceBound,可以避免重复解除绑定导致异常。
在 PlayerFragment 类的 onCreateView() 后、绑定方法前添加两个生命周期方法,分别在视图创建后绑定服务、视图销毁前解除绑定。
class PlayerFragment : Fragment() { // ... 省略已有字段和 onCreateView()。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 视图创建后开始绑定服务。 bindMusicService() }
override fun onDestroyView() { // 视图销毁前解除服务绑定。 unbindMusicService() super.onDestroyView() }
// ... 省略已添加的绑定与解绑方法。}4. 运行验证:PlayerFragment 服务绑定
运行应用,在 Android Studio 的 Logcat 中搜索 MusicLifecycle。截图中的前两行表示服务已创建并收到启动请求,第三行表示服务收到绑定请求,第四行表示 PlayerFragment 已连接服务。
五、为 MusicListFragment 绑定服务
1. 添加服务连接对象
打开 MusicListFragment.kt,添加服务绑定所需的导入、服务引用和连接回调。
import android.content.ComponentNameimport android.content.Contextimport android.content.Intentimport android.content.ServiceConnectionimport android.os.IBinderimport android.util.Log// ... 省略已有导入。import com.sziit.musicplayer.service.MusicService
class MusicListFragment : Fragment() { private lateinit var binding: FragmentMusicBinding private lateinit var adapter: MusicListAdapter private var musicService: MusicService? = null private var isServiceBound = false
private companion object { const val TAG = "MusicLifecycle" }
// 连接成功后通过 Binder 获取服务实例。 private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { musicService = (service as MusicService.MusicBinder).getService() Log.i(TAG, "MusicListFragment: 已连接 MusicService") }
override fun onServiceDisconnected(name: ComponentName) { musicService = null } }
// ... 省略已完成的生命周期方法。}2. 添加绑定与解绑方法
在类的末尾添加绑定与解绑方法:
class MusicListFragment : Fragment() { // ... 省略服务连接对象和生命周期方法。
private fun bindMusicService() { val intent = Intent(requireActivity(), MusicService::class.java) // 自动创建尚未运行的服务,并记录绑定状态。 requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) isServiceBound = true }
private fun unbindMusicService() { if (isServiceBound) { requireActivity().unbindService(serviceConnection) isServiceBound = false } // 释放服务引用,避免页面销毁后继续持有它。 musicService = null }}3. 在生命周期中绑定服务
在 MusicListFragment 的 onViewCreated() 中,RecyclerView 初始化后、调用 MusicApi.fetchSongs() 前绑定服务。这样接口返回歌曲时,Fragment 已取得服务实例,可以将歌曲交给服务处理。
class MusicListFragment : Fragment() { // ... 省略已有字段和其他生命周期方法。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // ... 省略 RecyclerView 初始化代码。 bindMusicService() MusicApi.fetchSongs( // ... 保留已有的成功和失败回调。 ) }}在 MusicListFragment 类中添加 onDestroyView(),在视图销毁时解除服务绑定:
class MusicListFragment : Fragment() { // ... 省略已有字段和生命周期方法。
override fun onDestroyView() { unbindMusicService() super.onDestroyView() }}4. 运行验证:两个 Fragment 均已连接服务
运行应用,在 Logcat 中搜索 MusicLifecycle。前 3 行记录服务的创建、启动和绑定请求;后 2 行表示 MusicListFragment 与 PlayerFragment 均已连接 MusicService。
任务小结
MainActivity 启动 MusicService,两个 Fragment 通过 Binder 取得同一个服务实例,并在视图销毁时解除绑定。Logcat 日志可以验证服务的启动和连接结果。