22、LiveData与Service:前台服务状态监听、使用LiveData更新服务状态、实战:音乐播放器
Service 和 LiveData 怎么结合?
这个问题,我当年刚接触 Android 时也困惑过。Service 跑在后台,LiveData 活在 ViewModel 里,它俩怎么通信?
说白了,就是让 Service 的状态变化,能被 UI 层感知到。今天我们就拿音乐播放器这个经典场景,把这件事彻底讲透。
为什么需要 LiveData 监听 Service 状态?
你想想看,一个音乐播放器在后台播放,用户在前台操作界面。播放、暂停、切歌、进度更新……这些状态变化,UI 必须实时响应。
传统的做法是用广播或者 Handler。但广播太重量级,Handler 又容易内存泄漏。LiveData 刚好解决了这个问题——它生命周期感知,不会泄漏,而且数据变化自动通知观察者。
我在项目中遇到过,早期用广播做 Service 和 Activity 通信,结果 Activity 被销毁重建后,广播接收器没解注册,直接崩溃。后来换成 LiveData,再也没出过这种问题。
核心思路:Service 内部持有 LiveData,当状态变化时更新它。UI 层通过 ViewModel 观察这个 LiveData,自动刷新界面。
前台服务的状态模型
我们先定义音乐播放器的状态。嗯,这里要注意,状态不要定义得太零散,否则维护起来很痛苦。
data class MusicPlayerState(
val isPlaying: Boolean = false,
val currentSong: Song? = null,
val progress: Int = 0, // 0-100
val duration: Int = 0, // 总时长(秒)
val error: String? = null
)
data class Song(
val id: Long,
val title: String,
val artist: String,
val duration: Int
)
我个人习惯把状态封装成一个不可变的数据类。每次状态变化,都生成一个新对象。这样 LiveData 才能正确触发更新。
Service 中如何使用 LiveData
Service 本身不直接暴露 LiveData 给 UI。我建议的做法是:Service 内部维护一个 MutableLiveData,然后通过一个公共方法暴露只读的 LiveData。
class MusicService : Service() {
companion object {
private val _state = MutableLiveData<MusicPlayerState>()
val state: LiveData<MusicPlayerState> get() = _state
}
private val binder = MusicBinder()
inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}
override fun onBind(intent: Intent?): IBinder = binder
fun play(song: Song) {
// 开始播放逻辑
_state.postValue(
_state.value?.copy(
isPlaying = true,
currentSong = song,
error = null
)
)
}
fun pause() {
// 暂停逻辑
_state.postValue(
_state.value?.copy(isPlaying = false)
)
}
fun updateProgress(progress: Int) {
_state.postValue(
_state.value?.copy(progress = progress)
)
}
}
注意:在 Service 中更新 LiveData 必须使用 postValue(),而不是 setValue()。因为 Service 可能运行在后台线程,而 setValue() 只能在主线程调用。
ViewModel 如何连接 Service 和 UI
ViewModel 通过绑定 Service 获取状态,然后暴露给 Fragment 或 Activity。
class MusicViewModel : ViewModel() {
private var musicService: MusicService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as MusicService.MusicBinder
musicService = binder.getService()
// 将 Service 的 LiveData 转发给 UI
_serviceState.addSource(musicService!!.state) { state ->
_serviceState.value = state
}
}
override fun onServiceDisconnected(name: ComponentName?) {
musicService = null
}
}
private val _serviceState = MediatorLiveData<MusicPlayerState>()
val serviceState: LiveData<MusicPlayerState> get() = _serviceState
fun bindService(context: Context) {
val intent = Intent(context, MusicService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
fun play(song: Song) {
musicService?.play(song)
}
fun pause() {
musicService?.pause()
}
override fun onCleared() {
super.onCleared()
// 记得解绑,防止泄漏
// 实际项目中需要在合适的时机调用 unbindService
}
}
这里我用 MediatorLiveData 做了一层转发。为什么?因为 Service 的 LiveData 是静态的,多个 ViewModel 实例共享同一个数据源。通过 MediatorLiveData 转发,每个 ViewModel 可以独立管理自己的观察者。
UI 层如何消费状态
Fragment 里观察 ViewModel 的状态,更新 UI 控件。
class MusicFragment : Fragment() {
private val viewModel: MusicViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.bindService(requireContext())
viewModel.serviceState.observe(viewLifecycleOwner) { state ->
updateUI(state)
}
// 播放按钮点击
binding.playButton.setOnClickListener {
val song = Song(1, "晴天", "周杰伦", 280)
viewModel.play(song)
}
}
private fun updateUI(state: MusicPlayerState) {
binding.playButton.text = if (state.isPlaying) "暂停" else "播放"
binding.songTitle.text = state.currentSong?.title ?: "未播放"
binding.progressBar.progress = state.progress
// 错误处理
state.error?.let {
showError(it)
}
}
}
小技巧:使用 viewLifecycleOwner 而不是 this 来观察 LiveData。这样当 Fragment 的视图销毁时,观察者会自动移除,避免空指针。
实战:完整音乐播放器状态流转
我把整个流程画了一张图,方便你理解数据是怎么流动的。
避坑指南
我曾经在项目里犯过一个低级错误:Service 的 LiveData 用了 setValue(),结果在后台线程更新时直接崩溃。后来排查了半天才发现是线程问题。
还有一次,我忘了在 onCleared() 里解绑 Service,导致 Activity 销毁后 Service 还持有 ViewModel 的引用,内存泄漏了。嗯,这两个坑你们一定要记住。
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| LiveData 不更新 UI | 在后台线程用了 setValue() | 改用 postValue() |
| 内存泄漏 | Service 绑定未解绑 | 在 onCleared() 或 onDestroy() 中解绑 |
| 状态丢失 | Activity 重建后 LiveData 重新订阅 | 使用 ViewModel 持有 LiveData,不依赖 Activity 生命周期 |
个人建议:如果你的音乐播放器需要持久化播放列表和当前歌曲,可以考虑把状态保存到 Room 数据库。Service 更新状态时同时写数据库,App 启动时从数据库恢复。这样即使进程被杀死,也能恢复播放状态。
好了,这一章的内容就到这里。LiveData 和 Service 的结合,说白了就是「生产者-消费者」模式。Service 生产状态,UI 消费状态。中间通过 ViewModel 做桥梁,既保证了生命周期安全,又实现了数据实时更新。
下一章我们会深入探讨 LiveData 的变换操作,比如 map 和 switchMap,让你能更灵活地处理复杂的数据流。
公众号:蓝海资料掘金营,微信deep3321