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 的视图销毁时,观察者会自动移除,避免空指针。

实战:完整音乐播放器状态流转

我把整个流程画了一张图,方便你理解数据是怎么流动的。

音乐播放器状态流转图 用户操作 点击播放/暂停 Fragment/Activity 调用 ViewModel 方法 ViewModel 转发操作到 Service MusicService 执行播放/暂停逻辑 LiveData 状态 postValue() 更新 UI 观察者 observe() 自动刷新 流程说明: 1. 用户点击播放 → Fragment 调用 ViewModel.play() 2. ViewModel 通过 Binder 调用 Service.play() 3. Service 更新 LiveData → UI 自动刷新

避坑指南

我曾经在项目里犯过一个低级错误: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