如何结合Architecture Components与MediaBrowserService构建音频应用架构?
很高兴你打算用Architecture Components重构你的MediaBrowserService音频应用,这绝对是个提升代码可维护性和扩展性的好选择!结合LiveData、ViewModel和Repository的分层架构,能让你的代码更清晰,也更容易处理生命周期和异步数据加载的问题。下面我会一步步拆解怎么搭建这个架构:
一、核心架构分层设计
先明确各层的职责,避免代码耦合:
- UI层:由Activity/Fragment + ViewModel组成,负责展示UI、响应用户交互,ViewModel持有LiveData暴露数据给UI层,同时处理与媒体服务的通信逻辑。
- 数据层:用Repository统一封装数据来源(网络、本地数据库等),ViewModel只和Repository交互,不用关心数据是从哪来的。
- 媒体服务层:改造后的
MediaBrowserServiceCompat,负责管理媒体会话、处理播放逻辑,同时通过onGetRoot()和onGetChildren()提供媒体数据(数据来源是Repository)。
二、ViewModel:连接UI与媒体服务的桥梁
ViewModel要负责管理MediaBrowser的生命周期,同时用LiveData暴露服务连接状态、媒体列表数据给UI层。这样UI层不用关心服务连接的细节,只需要观察LiveData即可。
示例代码(Kotlin):
class MediaViewModel( private val mediaRepository: MediaRepository, private val appContext: Context ) : ViewModel() { // 暴露给UI层的媒体列表LiveData private val _mediaItems = MutableLiveData<List<MediaBrowser.MediaItem>>() val mediaItems: LiveData<List<MediaBrowser.MediaItem>> = _mediaItems // 服务连接状态LiveData private val _isServiceConnected = MutableLiveData<Boolean>() val isServiceConnected: LiveData<Boolean> = _isServiceConnected private lateinit var mediaBrowser: MediaBrowser init { initMediaBrowser() } private fun initMediaBrowser() { mediaBrowser = MediaBrowser( appContext, ComponentName(appContext, YourMediaService::class.java), object : MediaBrowser.ConnectionCallback() { override fun onConnected() { _isServiceConnected.postValue(true) // 订阅根目录的媒体项 mediaBrowser.subscribe(mediaBrowser.root, mediaSubscriptionCallback) } override fun onConnectionSuspended() { _isServiceConnected.postValue(false) } override fun onConnectionFailed() { _isServiceConnected.postValue(false) } }, null ) mediaBrowser.connect() } private val mediaSubscriptionCallback = object : MediaBrowser.SubscriptionCallback() { override fun onChildrenLoaded(parentId: String, children: List<MediaBrowser.MediaItem>) { _mediaItems.postValue(children) } } // 播放控制:通过SessionToken发送命令给服务 fun playMedia(mediaId: String) { if (_isServiceConnected.value == true) { val playIntent = Intent(MediaSessionCompat.COMMAND_PLAY_FROM_MEDIA_ID) playIntent.putExtra(MediaBrowser.EXTRA_MEDIA_ID, mediaId) mediaBrowser.sessionToken.sendSessionCommand(playIntent, null) } } // ViewModel销毁时断开媒体服务连接 override fun onCleared() { super.onCleared() mediaBrowser.unsubscribe(mediaBrowser.root, mediaSubscriptionCallback) mediaBrowser.disconnect() } }
三、Repository:统一数据管理中心
Repository负责封装所有数据获取逻辑,不管是从网络拉取还是本地数据库读取,都在这里处理。这样ViewModel不用关心数据来源,只需要调用Repository的方法即可,同时还能实现数据缓存策略(比如优先读本地,再从网络刷新)。
示例代码:
class MediaRepository( private val mediaApi: MediaApi, private val mediaDao: MediaDao ) { // 协程处理异步数据加载 suspend fun getMediaItems(): List<Media> { // 先尝试读取本地数据库缓存 val localMedia = mediaDao.getAllMedia() if (localMedia.isNotEmpty()) { return localMedia } // 本地无数据时从网络拉取 val remoteMedia = mediaApi.fetchMediaList() // 保存到本地数据库 mediaDao.insertAll(remoteMedia) return remoteMedia } }
四、改造MediaBrowserService:对接Repository
原来的onGetChildren()方法需要改为从Repository获取数据,注意要异步处理(不能阻塞主线程),可以用协程或者单线程Executor。
示例代码:
class YourMediaService : MediaBrowserServiceCompat() { private lateinit var mediaRepository: MediaRepository private lateinit var mediaSession: MediaSessionCompat override fun onCreate() { super.onCreate() // 初始化Repository(推荐用Hilt做依赖注入) mediaRepository = MediaRepository( ApiClient.create(), MediaDatabase.getInstance(this).mediaDao() ) // 初始化媒体会话,处理播放逻辑 mediaSession = MediaSessionCompat(this, "YourMediaService").apply { setCallback(MediaSessionCallback()) isActive = true } sessionToken = mediaSession.sessionToken } override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { // 这里可以做权限校验,比如只允许自己的应用连接 return BrowserRoot("ROOT_MEDIA_ID", null) } override fun onLoadChildren(parentId: String, result: Result<List<MediaBrowser.MediaItem>>) { // 异步加载数据,避免阻塞主线程 lifecycleScope.launch(Dispatchers.IO) { val mediaList = mediaRepository.getMediaItems() // 转换为MediaBrowser需要的MediaItem格式 val mediaItems = mediaList.map { media -> MediaBrowser.MediaItem( MediaDescriptionCompat.Builder() .setMediaId(media.id) .setTitle(media.title) .setSubtitle(media.artist) .build(), MediaBrowser.MediaItem.FLAG_PLAYABLE ) } // 主线程返回结果 withContext(Dispatchers.Main) { result.sendResult(mediaItems) } } } // 媒体会话回调,处理播放、暂停等命令 private inner class MediaSessionCallback : MediaSessionCompat.Callback() { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { mediaId?.let { // 这里实现播放逻辑,比如创建MediaPlayer或者ExoPlayer } } // 其他播放控制方法:onPause、onStop等 } }
五、UI层:观察LiveData更新界面
在Fragment/Activity中,只需要观察ViewModel的LiveData,更新列表和UI状态即可,不用处理复杂的异步或服务连接逻辑。
示例代码:
class MediaListFragment : Fragment() { private lateinit var viewModel: MediaViewModel private lateinit var mediaAdapter: MediaAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_media_list, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 初始化ViewModel(推荐用Hilt注入) viewModel = ViewModelProvider(this)[MediaViewModel::class.java] // 初始化列表Adapter mediaAdapter = MediaAdapter { mediaId -> viewModel.playMedia(mediaId) } binding.mediaRecyclerView.adapter = mediaAdapter // 观察媒体列表更新UI viewModel.mediaItems.observe(viewLifecycleOwner) { items -> mediaAdapter.submitList(items) } // 观察服务连接状态,显示加载/错误状态 viewModel.isServiceConnected.observe(viewLifecycleOwner) { isConnected -> binding.progressBar.isVisible = !isConnected binding.errorLayout.isVisible = !isConnected } } }
额外建议
- 依赖注入:用Hilt或者Dagger来管理ViewModel、Repository、API实例等,避免手动实例化带来的耦合问题。
- 协程替代AsyncTask/Loader:协程比传统的异步方式更简洁,也更容易处理异常和生命周期。
- 离线缓存:借助Room数据库实现媒体数据的离线缓存,提升用户体验。
- 媒体会话管理:确保
MediaSession的生命周期和服务绑定,正确处理播放状态的持久化。
内容的提问来源于stack exchange,提问作者wenguang huang
相关产品推荐
相关产品推荐

