4. Tiles与信息展示

Tiles 是 Wear OS 上最贴近“抬腕即看”的界面形态。它不像 Activity 那样由用户主动打开并长时间停留,而是由系统在表盘滑动、卡片轮播等场景中按需请求、按需渲染。理解这一点,是写好 Tile 的前提:Tile 不是常驻页面,而是一段由系统调度、可被随时重建的轻量信息视图。因此,TileService 的生命周期、数据更新时机和缓存策略,直接决定了用户看到的是“实时可信的信息”还是“过期甚至空白”的卡片。

TileService 生命周期:谁在什么时候调用你

TileService 继承自系统服务,其生命周期与普通 Service 有本质区别。它并不由应用自行启动,而是由系统在需要展示 Tile 时绑定并调用。核心回调包括:

  • onTileAdded():用户首次将 Tile 添加到轮播列表时调用,适合做一次性初始化,例如注册数据监听、写入默认配置。
  • onTileRemoved():用户移除 Tile 时调用,应在此释放监听、取消定时任务,避免后台资源泄漏。
  • onStartListening():Tile 进入可见或即将可见状态时调用,是刷新数据、请求最新内容的主要入口。
  • onStopListening():Tile 离开可见状态时调用,应停止高频刷新、暂停轮询,降低功耗。
  • onTileRequest():系统请求渲染内容时调用,必须在此返回一个 Tile 对象,描述布局与数据。
  • onTileResourcesRequest():系统请求图片等资源时调用,用于按需提供图标或图片。

需要特别注意的是,onTileRequest() 可能在任何时候被调用,包括应用进程刚被拉起、缓存尚未就绪时。因此,渲染逻辑必须能在没有网络、没有最新数据的情况下给出可接受的降级结果,而不是抛异常或返回空布局。

TileService 生命周期与数据流 系统调度 Wear OS 框架 用户添加 Tile Tile 进入可见 请求渲染内容 Tile 离开可见 用户移除 Tile TileService 回调 应用进程 onTileAdded() onStartListening() onTileRequest() onStopListening() onTileRemoved() 数据与缓存 本地存储 / 网络 初始化监听 读取缓存 请求最新数据 暂停刷新 释放资源 返回 Tile 布局与数据 关键原则:onTileRequest 必须能在无网络、无最新缓存时返回可接受的降级布局

动态数据更新:在正确的时间做正确的事

Tile 的数据更新不是“越频繁越好”。系统对 Tile 的刷新有明确约束,过度刷新会导致功耗上升,甚至被系统限制。合理的做法是分层处理:

第一层:onStartListening 触发刷新。当 Tile 进入可见状态时,先立即用本地缓存渲染一版,保证用户抬腕即有内容;同时发起异步请求获取最新数据,拿到后再通过 getUpdater() 请求系统重新渲染。这样用户不会看到空白等待。

第二层:版本化数据模型。每次数据变更时递增一个版本号或时间戳,写入本地存储。Tile 渲染时读取该版本,若版本未变则直接复用上次布局,避免无意义的重建。示例数据结构可以设计为:

data class TileData(
    val version: Long,        // 数据版本号,每次更新递增
    val title: String,        // 主标题
    val value: String,        // 核心数值
    val updatedAt: Long,      // 更新时间戳
    val isStale: Boolean      // 是否已过期
)

第三层:过期判断与降级。渲染时检查 updatedAt 与当前时间的差值。若超过预设阈值(例如 15 分钟),将 isStale 置为 true,在 Tile 上显示“数据可能已过期”的提示,而不是继续展示旧值。这比直接显示错误或空白更友好。

建议将刷新阈值与业务语义绑定:天气类 Tile 可容忍 30 分钟,股票或计时类 Tile 可能需要 1 分钟以内。阈值应作为常量集中管理,便于调整。

用户交互:Tile 上的点击与跳转

Tile 支持有限的交互,主要是点击整个 Tile 或点击某个按钮区域。交互通过 Action 和 Clickable 定义,最终转化为一个 PendingIntent,由系统在用户点击时触发。常见做法是:

  • 点击 Tile 主体,跳转到对应的 Activity 查看详情。
  • 点击“刷新”按钮,触发一次手动数据更新,并通过 getUpdater().requestUpdate() 请求重绘。
  • 点击“切换”按钮,在两种展示模式之间切换,例如“今日/本周”。

需要注意的是,Tile 上的交互不应执行耗时操作。点击后应尽快返回,把实际处理交给 Activity 或后台任务。否则系统可能因响应超时而判定 Tile 无响应。

生产环境缓存策略:让 Tile 在弱网下依然可用

生产环境中,网络不可靠是常态。Tile 的缓存策略应遵循“先缓存后网络、先可用后精确”的原则。具体可以分三步落地:

写入时机。每次成功获取数据后,立即写入本地存储(推荐使用 DataStore 或轻量数据库)。写入内容包含数据本身、版本号和时间戳。写入操作应放在后台线程,避免阻塞 Tile 渲染。

读取时机。onTileRequest() 被调用时,优先读取本地缓存。若缓存存在且未过期,直接渲染;若缓存存在但已过期,渲染缓存内容并附加过期提示;若缓存不存在,渲染默认占位布局,并触发一次异步请求。

清理时机。在 onTileRemoved() 中清理与该 Tile 相关的临时缓存,但保留用户配置类数据。避免长期占用存储空间,也避免用户重新添加 Tile 时丢失个性化设置。

不要在 onTileRequest() 中执行网络请求或数据库全量查询。该回调有严格的执行时间限制,超时会导致 Tile 渲染失败。所有耗时操作都应提前完成或异步触发。

一个可参考的缓存决策表

下表给出不同缓存状态下的渲染策略,可作为实现时的判断依据。表中“过期阈值”以 15 分钟为例,实际项目中应按业务调整。

缓存状态 判断条件 渲染策略 是否触发异步刷新
无缓存 本地无记录 显示默认占位布局 是
缓存有效 now - updatedAt ≤ 15min 直接渲染缓存数据 否
缓存过期 now - updatedAt > 15min 渲染缓存数据 + 过期提示 是
缓存损坏 解析失败或字段缺失 清除缓存,显示占位布局 是

把这张表落到代码里,核心就是在 onTileRequest() 中做一次状态判断,然后走对应的渲染分支。判断逻辑本身很轻,不会影响渲染性能,但能显著提升弱网和冷启动场景下的用户体验。

最后要强调的是,Tile 的稳定性不仅取决于代码正确,还取决于对系统调度边界的尊重。不要在 onTileRequest() 里做重活,不要在 onStopListening() 后继续刷新,不要假设缓存一定存在。把这三条守住,Tile 在真实设备上的表现就会稳定得多。