相关代码
kotlin
代码1:
AlbumDetailsAdapter.kt:
/**
* list item
*/
interface IAlbumDetailsItem {
fun id(): String
fun name(): String?
fun author(): String?
fun vipContent(): Boolean // 是否是 vip 内容
fun playFlag(): Boolean // 是否可以播放
fun vipPlayFlag(): Boolean // 是否会员可以播放
fun getAlbumDetails(): IAlbumDetailsHead?
fun getPlayable(): IPlayable?
fun playerList(): MutableList<IPlayable>?
fun setHeadData(head: IAlbumDetailsHead?)
}
@ExcludeFromGeneratedCodeCoverage
data class AlbumDetailsItem(
val id: String,
val name: String?,
val author: String?,
val isPlayFlag: Boolean,
val authTrackPlayType: Int,
var data: IAlbumDetailsHead?,
val isVipContent: Boolean,
val iPlayable: IPlayable?,
val playerList: MutableList<IPlayable>? = null
) : IAlbumDetailsItem {
override fun id() = id
override fun name() = name ?: ""
override fun author() = author ?: ""
override fun playFlag(): Boolean = isPlayFlag
override fun vipPlayFlag(): Boolean = authTrackPlayType == ContentConstant.TYPE_TRACK_AUTH_VIP_ANY
override fun vipContent(): Boolean = isVipContent
override fun getAlbumDetails(): IAlbumDetailsHead? = data
override fun getPlayable(): IPlayable? = iPlayable
override fun playerList() = playerList
override fun setHeadData(head: IAlbumDetailsHead?) {
data = head
}
}
代码2:
@ExcludeFromGeneratedCodeCoverage
interface IPlayable {
fun id(): String
fun kind(): String
fun title(): String
fun parentId(): String?
fun parentTitle(): String?
fun childId(): String?
fun announcerId(): String?
fun announcerName(): String?
fun duration(): Long
fun playUrl(highFirst: Boolean): String?
fun cover(): String?
fun playCount(): String
// recommend source
fun recSrc(): String?
fun recTrack(): String?
fun playSource(): String?
fun isSomedayCurrentTimeProgram(): Boolean
fun isLive(): Boolean = false
fun livingProgress(): Long = 0
fun setAlternativeCover(url: String?) {}
// for track or music
fun parentAlbum(): IAlbum?
// for radio
fun parentRadio(): IPlayable?
fun setRadio(radio: IPlayable) {}
fun isPlayingRadioName(): String = ""
fun programTime(): String = ""
fun updateDuration(durReal: Long) {}
fun authTrackType(): Int = ContentConstant.TYPE_TRACK_AUTH_FREE
fun authTrackPlayType(): Int = ContentConstant.TYPE_TRACK_AUTH_FREE
fun isVipContent(): Boolean = false
fun playFlag(): Boolean = true
fun sortNumber(): Int = 0
fun hadPurchased(): Boolean = false
// for music record upload
fun setBitrate(bitrate: Int?) {}
fun getBitrate(): Int? = null
fun plLevel(): String? = null
fun isLiked(): Boolean = false
fun setLiked(liked: Boolean) {}
fun getSoundQuality(): ISoundQuality? {
return null
}
fun artists(): List<IArtist>? = null
// 应该调用这个方法,如果默认实现冲突,则应该在实现中调用某个接口的默认实现(例如playableToMetadata),或自行实现
fun toMetadata(): MediaMetadata = playableToMetadata().build()
fun isFreeTrail(): Boolean = false
fun playUri(): Uri? = null
fun coverUri(): Uri? = null
// 只应该在 toMetadata 方法(默认实现或实现类实现中)被调用
fun playableToMetadata(): MediaMetadata.Builder {
val mediaMetadata = MediaMetadata.Builder()
mediaMetadata.putString(MediaMetadata.METADATA_KEY_TITLE, title())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_ARTIST, announcerName())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_ALBUM, parentTitle())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_AUTHOR, announcerName())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_ART_URI, cover())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, cover())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, announcerName())
mediaMetadata.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, cover())
when (kind()) {
ContentConstant.KIND_LOCAL_MUSIC,
ContentConstant.KIND_TRACK -> {
mediaMetadata.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id())
mediaMetadata.putLong(
MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
MediaDescription.BT_FOLDER_TYPE_TITLES
)
}
ContentConstant.KIND_MUSIC -> {
mediaMetadata.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id())
mediaMetadata.putLong(
MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
MediaDescription.BT_FOLDER_TYPE_TITLES
)
mediaMetadata.putRating(MediaMetadata.METADATA_KEY_RATING, Rating.newHeartRating(isLiked()))
}
ContentConstant.KIND_RADIO -> {
mediaMetadata.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id())
mediaMetadata.putLong(
MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
MediaDescription.BT_FOLDER_TYPE_YEARS
)
}
ContentConstant.KIND_SCHEDULE -> {
mediaMetadata.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, parentId())
mediaMetadata.putLong(
MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
MediaDescription.BT_FOLDER_TYPE_YEARS
)
}
ContentConstant.KIND_PODCAST -> {
mediaMetadata.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id())
mediaMetadata.putLong(
MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
MediaDescription.BT_FOLDER_TYPE_MIXED
)
}
}
mediaMetadata.putLong(MediaMetadata.METADATA_KEY_DURATION, duration())
return mediaMetadata
}
}
技术设计文档:车机多媒体系统中基于"快递盒模型"的防腐层设计## 1. 概述与背景
在大型 Android 汽车车载娱乐系统(IVI)开发中,音频播放模块(如音乐、播客、有声书、广播)是其最核心的业务之一。此类系统通常面临两大痛点:
- 底层领域模型极度复杂:需要处理蓝牙同步、系统媒体中心(MediaSession)映射、车速音量补偿、网络码率切换以及不同音频源的字段差异。
- 上层 UI 视觉高频迭代:针对不同车机屏幕形态(如直板屏、折叠屏、分屏模式、仪表盘副屏),UI 展现形式各异,且伴随车型升级经常发生重构。
为了同时满足底层核心业务的健壮性与上层 UI 的高扩展性,本项目采用了"快递盒模型"(即防腐层模式 / Anti-corruption Layer)进行架构解耦。
本篇文档将深入剖析 IAlbumDetailsItem(UI 视图模型/快递盒)与 IPlayable(底层领域模型/贵重物品)之间的复合结构设计,并系统阐述其背后的设计哲学。
2. 架构拓扑与核心分层
本系统的核心代码设计严格遵循了由下至上的依赖流向,确保 UI 与播放内核不直接产生硬编码耦合。
2.1 架构模型定义
┌────────────────────────────────────────────────────────┐
│ UI 表现层 (Presentation Layer) │
│ 消费: IAlbumDetailsItem (仅读取 name, vipContent 等) │
└──────────────────────────┬─────────────────────────────┘
│
快递盒解包 -> 当发生用户切歌点击事件时
│
┌──────────────────────────▼─────────────────────────────┐
│ 领域播放内核层 (Core Playback Layer) │
│ 消费: IPlayable (执行 playUrl, 转化 MediaMetadata ) │
└────────────────────────────────────────────────────────┘
- IPlayable(贵重物品):底层的、纯粹的音频领域核心模型(Domain Model)。其生命周期常驻于底层播放器服务中,不掺杂任何特定 UI 渲染属性,主要对 Android 多媒体服务(MediaMedia)、车载蓝牙规范(AVRCP)负责。
- IAlbumDetailsItem(快递盒):上层的、专属的UI 列表视图模型(UI Model / View Model)。它是一个高内聚、低耦合的数据外壳,专门用于保障车机列表的高性能渲染。
3. "快递盒模型"的设计价值核心## 3.1 物理内存的"轻量化零克隆"(指针传递)
误区: 认为在 AlbumDetailsItem 中嵌套一个 IPlayable 对象会导致对象数据膨胀、消耗过大车机内存。
事实:
在 JVM/Art 虚拟机底层,val iPlayable: IPlayable? 存储的仅仅是一个 64位(8字节)的内存地址指针。
- 零开销数据共享:将 IPlayable 注入 AlbumDetailsItem 的过程,并没有对底层的长字符串、音频 URL、位图进行任何 clone() 或内存复制,仅仅是复制了一个极其微小的地址数字。
- 内存影响:无论 IPlayable 内部有多少方法、数据有多庞大,它在整个列表滑动、渲染过程中,给每个条目带来的物理内存增量永远只有 8 个字节。
3.2 界面隔离与防腐层机制(保护底层内核)
如果让 UI 的 RecyclerView.Adapter 或 MediaItemView 直接去强依赖并绑定 IPlayable,会引发严重的架构灾难:
- 代码污染:UI 开发者在写列表时,可能会不小心在滚动时误调用 playable.updateDuration() 或修改其状态,导致底层的音频播放状态错乱。
- 防腐层(ACL):AlbumDetailsItem 扮演了隔离防腐层。在列表日常渲染(展示文本、标记是否是 VIP 歌曲、判定是否可以点击)时,通过外壳接口(name()、vipContent())提供干净的数据。它切断了 UI 对底层繁杂状态的直接可见性,有效防止了业务逻辑向上"腐蚀"和泛滥。
3.3 零延迟的跨层状态传递
当车机屏幕发生用户交互(例如用户点击了列表中的某一行执行"切歌"),播放内核面临两个刚性需求:
- 内核必须立即拿到该歌的 playUrl() 去执行网络流媒体解码。
- 内核必须立即调用 playableToMetadata() 生成原生的 MediaMetadata 对象,同步给车辆仪表盘(Cluster)和 抬头显示(HUD) 以防出现切歌时的界面不同步。
// 完美的交互模型:UI 抛出快递盒中的核心资产
adapter.setOnItemClickListener { _, _, position ->
val item = adapter.getItem(position)
val playable = item.getPlayable() // 0毫秒延迟,直接提取出原生的领域实体
if (playable != null) {
mediaPlaybackService.play(playable) // 直接砸给底层播放器服务
}
}
- 如果不用快递盒模型:UI 层点击后只能抛出一个 id: String。底层服务为了开始放歌,必须重新在后台开启一条线程,去查本地 SQLite 数据库或发网络请求查询这首歌的完整元数据(Duration, URL, Bitrate...)。
- 车载系统硬指标:车载系统对"切歌响应延迟(TTPl - Time to Play)"有着严苛的考核(通常要求小于 500ms)。通过快递盒携带指针的设计,实现了 0 毫秒的无缝对接,保障了车机交互的丝滑体验。
3.4 抹平多模态 UI 数据源的差异
在车机多媒体系统中,详情页列表的展现形式是非常多元的:
- 音乐模式:展示歌曲名、歌手。
- 播客/有声书模式:展示单集标题、演播人、音轨音质。
- 特殊行:列表第一行是"专辑封面大卡片(Header)",中间可能会插入一条"广告 Banner"。
通过抽象出 IAlbumDetailsItem 接口,它成功地抹平了底层各种异构数据的差异。不论是音乐的 IPlayable,还是播客的 IPlayable,甚至是一个广告数据模型,它们都可以实现 IAlbumDetailsItem 接口。对于上层的 AlbumDetailsAdapter 而言,它不需要写一堆 if-else 去判断底层类型,实现了真正的多态扩展。
4. 最佳实践与规约建议
为了将该设计模式的优势发挥到极致,并在团队开发中形成资产沉淀,提出以下规约指南:
- UI 渲染期只读外壳:严禁在 RecyclerView 的 convert() / onBindViewHolder() 中通过 item.getPlayable()?.xxx 来读取展示文本。凡是界面展示需要的字段,必须由 ViewModel 在映射期完成,并声明在 IAlbumDetailsItem 外壳接口中。
- 点击事件直接外传:UI 列表不处理任何具体的播放逻辑。在 OnClickListener 触发时,应迅速通过 item.getPlayable() 取出物品,以事件(MVI Event / Click 回调)的形式将其无状态地向上抛给 ViewModel 或 Service 处理。
- 单向数据流向:外壳模型(AlbumDetailsItem)应该保持属性的只读性(Immutable),当底层音频状态发生变化(如切歌导致高亮、收藏状态变更)时,应由 ViewModel 触发整体映射替换,或利用局部增量刷新(Payloads)机制刷新外壳,避免直接破坏领域模型的单一真相源。
5. 总结
IAlbumDetailsItem 与 IPlayable 的结合,是"面向接口编程"和"单一职责原则"在大型车载项目中的完美映射。它用极其微小的指针内存开销(8字节),换取了架构高隔离性、UI 多模态适配能力、以及车机音视频秒开的高吞吐性能。在后续系统的迭代升级中,应当坚定地沿用和推广该"快递盒"设计理念。
如果想继续深入探讨此设计,可以告诉我:
- 是否需要了解车机系统在极端弱网环境下,这个"快递盒"模型如何配合本地二级缓存(Room)来实现数据的快速冷启动?
- 是否需要看一份关于此结构在 Compose 声明式 UI 下的完整重构示范?