本文主要介绍 Android 媒体库客户端的两层结构决策:工程上为何坚持单一 App 产物 + 双 Interaction Shell;TV 界面上为何在行业仍以 Leanback 为主时,继续选择 Compose for TV 并拒绝把 Leanback 接回主线。
以 Octans Android 为例。播放内核、认证、API 与数据层希望全端共享;交互上 TV 又绝不是「平板横屏」。若过早拆 app-mobile / app-tv,或跟风把 TV 重写成 Leanback,会把成本花在错误的边界上。
决策一:单 App + 双 Interaction Shell
目标形态
single Android app artifact
+ shared core / data / domain / playback
+ touch interaction shell
+ TV interaction shell
+ 统一播放引擎与 runtime 治理
- 只保留一个 Gradle App module(例如
:app)。 - 手机、平板、Android TV / Google TV 安装同一应用产物(可按渠道 flavor 区分包名,但不等于两套业务工程)。
- 隔离的是 交互模型、焦点、导航图、布局,不是登录态、播放 session、LGPL runtime 与许可证审计。
交互模式
InteractionMode = Auto | Touch | Tv
| 模式 | 含义 |
|---|---|
| Auto | 按设备能力进入对应 shell(判定实现以产品实现文档为准) |
| Touch | 触控优先,适配窗口尺寸与横竖屏 |
| Tv | 遥控器 / 10-foot,独立返回栈与播放控制 |
交互模式不应分叉播放 engine、认证、repository 与 runtime 制品。
为什么拒绝双 App 入口
app-mobile + app-tv 在概念上清晰,但在个人主线 / 中小团队阶段会让:
- Gradle、manifest、依赖、runtime artifact
- 登录态、播放 engine、版本号、CI、装机验证
全部过早分裂。播放主线若已固定为单一 native 内核(如 LGPL libmpv),更应共享工程入口,只在 UI shell 处分叉。
为什么拒绝「一个响应式 UI 通吃 TV」
手机与平板可以共用触控 shell 做自适应;TV 不是平板横屏:
- 焦点与 D-pad / 返回键语义
- 10-foot 信息密度与 overscan
- 播放控制与空状态
必须允许独立设计。单 App 解决的是工程与契约统一,不是 UI 偷懒。
演进约束
- 首版可先打通触控 compact shell + 播放 MVP,TV shell 后置,但目录与决策上预留,不反向拆双 App。
- 只有代码体量与依赖边界真实变大时,再抽 Gradle module;不要为历史占位空模块复辟。
决策二:TV UI 框架继续 Compose for TV
背景张力
真机上 TV 首页焦点滚动可能「不跟手、中途停顿」。对照多款主流 Android TV 媒体客户端,静态分析常见路径是:
Leanback / View Presenter + GridView 回收
→ 选中一次滚动到位 的手感模型
问题于是变成:是否为了滚动手感,把 TV 主线切回 Leanback?
决策
TV interaction shell = Compose for TV / tv-material
+ foundation 列表与焦点 / bringIntoView 工程化
+ 不引入 Leanback Presenter / Browse 模板作为默认架构
与 touch 共享:认证、domain、data、播放 engine、设计语义。若局部需要 View 互操作,只能是可删除实验,不能反客为主。
行业对照(观察级,非竞品永久承诺)
| 类型 | TV UI 主架构(样本期) | 含义 |
|---|---|---|
| 多款存量媒体 TV 客户端 | Leanback 族(Presenter + Grid) | 历史定型 + 迁移成本 |
| 官方方向 | Leanback deprecated;新项目推荐 Compose for TV | 时间线:手机 Compose 成熟早于 TV Material |
| 本项目 | Compose for TV | 与 touch 同栈,滚动问题当工程债还 |
Compose for TV 相对新(tv-material 稳定晚于手机 Compose),存量客户端继续 Leanback 不等于「Compose 做不了 TV」,更像路径依赖。
拒绝的备选
| 方案 | 结论 | 原因 |
|---|---|---|
| Leanback 作为 TV 主 UI | 拒绝 | 等于第二套 TV 前端;与 touch Compose 分裂;官方已 deprecate |
| 首页 Leanback、其余 Compose 混合 | 拒绝作默认 | 焦点、导航、生命周期与主题双轨,债务高于单栈攻坚 |
| 为滚动问题推倒重写 | 拒绝 | 收益主要是抄滚动模型,不是产品能力增量 |
与「实验更卡」记录的关系
若未提交的「性能 / 焦点」工作区实验体感更差,应整包回退到已知好提交,用单变量对照再优化;不要据此写成「Compose 路线失败」或「必须 Leanback」。观察级对照 ≠ 根因定论。
两层决策如何叠在一起
工程:single app + shared playback/auth/data
│
├─ touch shell(Compose / Material)
└─ TV shell(Compose for TV)
│
└─ 滚动 / 焦点持续工程化
(不切换 Leanback 主线)
播放内核演进(libmpv / Media3 实验)发生在 playback 层,不改变 shell 选型;UI 排障(Paging、滚动状态、会话恢复)发生在 touch shell 实现,也不改变「单 App」合同。
对同类产品的启示
- 先统一 artifact 与播放契约,再分叉交互;双 APK 是规模化选项,不是起步默认。
- TV 独立 shell 是产品要求,双 App 是工程选项——两者不要绑死。
- 行业手感来自长期 View 工程,不是 Leanback 商标魔法;换栈前算清重写面。
- 官方 deprecate 方向应进入长期决策,避免新主线往死路上堆。
- 性能实验要可回退、可二分;不要用「感觉卡」直接否定框架选型。
注意事项
- 设备身份 / Auto 判定细节以实现文档为准,本文只固定「应有 shell 分叉」。
- 竞品 APK 分析会随版本变化,表中结论仅作选型背景。
- 本文不承诺 TV 滚动已达竞品手感;只固定框架主线与工程策略。
相关阅读
- Octans Android 播放内核演进:从 libmpv 单内核到双内核实验
- Android 媒体客户端 Compose 排障合集:滚动、闪烁、会话恢复
- Windows 媒体客户端 UI 路线:WebView2 复用、WinUI 3 候选与当前主线
来源
本文改写自 Octans 项目「Android 单 App 与双 Interaction Shell」决策,以及「Android TV UI 框架:Compose for TV」决策。行业对照为样本期静态分析摘要;实现路径已抽象。