You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何结合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
        }
    }
}
额外建议
  1. 依赖注入:用Hilt或者Dagger来管理ViewModel、Repository、API实例等,避免手动实例化带来的耦合问题。
  2. 协程替代AsyncTask/Loader:协程比传统的异步方式更简洁,也更容易处理异常和生命周期。
  3. 离线缓存:借助Room数据库实现媒体数据的离线缓存,提升用户体验。
  4. 媒体会话管理:确保MediaSession的生命周期和服务绑定,正确处理播放状态的持久化。

内容的提问来源于stack exchange,提问作者wenguang huang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:16:56