如何结合MVVM使用绑定服务?解决旋转时前台服务重启问题
我明白你遇到的困扰了——手机旋转触发配置变更后,你的PodcastPlayerService居然重启了,而不是和重建后的AudioPlayerFragment重新绑定。这确实是绑定服务搭配配置变更时的常见坑,咱们一步步来理顺解决方案:
先搞清楚问题根源
前台服务本身不会因为配置变更自动重启,因为它的生命周期独立于Activity/Fragment。你遇到的服务重启,大概率是因为Fragment重建时,你又重复调用了startForegroundService(),导致服务的初始化逻辑被重复执行;另外绑定时机的问题也可能让你误以为服务重启了。
具体解决方案
1. 避免重复启动服务
首先要修改服务启动逻辑,确保只有当服务未运行时才启动它,否则只同步必要的信息:
private fun startPodcastPlayerService(podcast: Podcast) { val intent = Intent(context, PodcastPlayerService::class.java) val serviceBundle = Bundle() serviceBundle.putParcelable(ExtrasKeyAndValues.KEY_PODCAST, podcast) intent.putExtra(ExtrasKeyAndValues.BUNDLE_PODCAST_SERVICE, serviceBundle) // 先检查服务是否已在运行 if (!isServiceRunning(PodcastPlayerService::class.java)) { context?.let { Util.startForegroundService(it, intent) } } else { // 服务已运行,只需发送Intent更新内容(如果需要切换播客) context?.startService(intent) } } // 辅助方法:检查指定服务是否正在运行 private fun isServiceRunning(serviceClass: Class<*>): Boolean { val activityManager = context?.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager return activityManager.getRunningServices(Int.MAX_VALUE) .any { it.service.className == serviceClass.name } }
2. 调整Fragment的绑定与UI初始化逻辑
当前你在onStart()里直接调用initPlayer(),但bindService()是异步操作,这时候isBound可能还是false,导致PlayerView无法正确关联到ExoPlayer。而且配置变更后,应该等待服务绑定完成后再初始化UI:
override fun onStart() { super.onStart() Intent(activity, PodcastPlayerService::class.java).also { intent -> activity?.bindService(intent, connection, Context.BIND_AUTO_CREATE) } // 移除这里的initPlayer(),交给onServiceConnected回调处理 } private val connection = object: ServiceConnection { override fun onServiceConnected(p0: ComponentName?, iBinder: IBinder?) { val binder = iBinder as PodcastPlayerService.LocalBinder podcastPlayerService = binder.getService() isBound = true // 绑定完成后再初始化PlayerView,不管是首次启动还是配置变更后重建 initPlayer() // 可选:同步当前播客到服务(比如用户在其他页面切换了播客) syncCurrentPodcastToService() } override fun onServiceDisconnected(p0: ComponentName?) { isBound = false } }
3. 优化服务的生命周期逻辑
确保服务只在首次创建时初始化ExoPlayer,onStartCommand()只处理内容更新,避免重复初始化:
class PodcastPlayerService : Service() { private lateinit var player: SimpleExoPlayer private var currentPodcast: Podcast? = null override fun onCreate() { super.onCreate() // 仅在服务创建时初始化一次ExoPlayer player = SimpleExoPlayer.Builder(this).build() // 启动前台通知的逻辑放在这里... } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.getBundleExtra(ExtrasKeyAndValues.BUNDLE_PODCAST_SERVICE)?.let { bundle -> val newPodcast = bundle.getParcelable<Podcast>(ExtrasKeyAndValues.KEY_PODCAST) newPodcast?.let { // 只有当播客不同时才切换播放源 if (currentPodcast?.id != it.id) { currentPodcast = it preparePlayer(it) } } } // 返回START_STICKY确保服务被系统杀死后能自动重启(按需选择) return START_STICKY } private fun preparePlayer(podcast: Podcast) { // 配置ExoPlayer媒体源的逻辑... } // LocalBinder实现(保持你的原有逻辑) inner class LocalBinder : Binder() { fun getService(): PodcastPlayerService = this@PodcastPlayerService } override fun onBind(intent: Intent): IBinder = LocalBinder() }
4. 用ViewModel保存Fragment状态(可选但推荐)
虽然不能把服务放进ViewModel,但可以用它保存当前播放的播客信息,避免配置变更后重新从arguments读取,让状态更稳定:
@HiltViewModel class AudioPlayerViewModel @Inject constructor() : ViewModel() { var currentPodcast: Podcast? = null } // 在Fragment中使用: @AndroidEntryPoint class AudioPlayerFragment: Fragment() { private val viewModel: AudioPlayerViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 优先从ViewModel取播客,没有的话再从arguments获取 viewModel.currentPodcast = viewModel.currentPodcast ?: arguments?.getParcelable(ExtrasKeyAndValues.KEY_PODCAST)!! podcast = viewModel.currentPodcast!! // 原有UI初始化逻辑... binding.title.text = podcast.title binding.description.text = podcast.description startPodcastPlayerService(podcast) // ... } }
核心思路总结
让前台服务保持独立运行,Fragment只负责绑定服务和同步UI状态,避免在配置变更时重复启动服务;利用服务绑定的异步回调,在重建后的Fragment中重新关联到已运行的服务和ExoPlayer实例,这样就能实现旋转时服务不重启,UI无缝衔接。
内容的提问来源于stack exchange,提问作者krtkush

