12、推荐系统:Recommendations API、通知栏推荐与内容更新
说实话,做TV应用最头疼的问题之一,就是用户打开电视后不知道看什么。手机可以刷,电视不行。遥控器按来按去,用户就烦了。这时候,推荐系统就派上用场了。
Android TV的推荐系统,说白了就是帮用户“猜心思”。你猜得准,用户就留下来。猜不准,用户就切到别的App去了。今天我就把这块的实战经验掰开揉碎讲给你听。
12.1 Recommendations API 的核心机制
Android TV的推荐,不是我们自己写算法。Google已经帮我们搭好了架子。我们只需要把内容“喂”给系统,系统会帮我们展示在Launcher的主屏上。
这个机制的核心,就是 RecommendationRequest 和 RecommendationResponse。嗯,其实更准确地说,是 Recommendations API 配合 Notification 来工作。
我刚开始接触这块时,以为要自己写推荐算法。后来发现,Google的思路是:你提供内容,系统负责展示。说白了,我们只需要做好“内容提供者”这个角色。
核心流程:
- App 构建推荐卡片(Recommendation)
- 通过 NotificationManager 发布到系统
- 系统在 Launcher 主屏展示推荐内容
- 用户点击后跳转到 App 指定页面
12.2 构建推荐卡片
推荐卡片长什么样?说白了就是一个 Notification。只不过这个Notification比较特殊,它带了很多TV专用的属性。
来看一段我实际项目中的代码:
public class RecommendationBuilder {
private static final String CHANNEL_ID = "recommendation_channel";
public static Notification buildRecommendation(Context context,
String title,
String description,
String imageUri,
String contentId) {
// 创建Intent,用户点击卡片后跳转
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra("content_id", contentId);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 构建推荐卡片
Notification notification = new Notification.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(description)
.setSmallIcon(R.drawable.ic_recommendation)
.setLargeIcon(loadBitmap(imageUri)) // 注意:这里要异步加载
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.build();
return notification;
}
}
小提示:
我在项目中踩过一个坑:setLargeIcon 如果传了太大的Bitmap,会导致通知发布失败。建议压缩到 512x512 以内,或者直接用Glide加载。
12.3 发布推荐到系统
卡片构建好了,怎么让系统知道?通过 NotificationManager。但这里有个关键点:每条推荐都需要一个唯一的ID。
public class RecommendationPublisher {
private static final int RECOMMENDATION_ID_BASE = 1000;
public static void publishRecommendation(Context context,
Notification notification,
int index) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 创建通知渠道(Android 8.0+ 必须)
NotificationChannel channel = new NotificationChannel(
"recommendation_channel",
"内容推荐",
NotificationManager.IMPORTANCE_HIGH
);
channel.setShowBadge(false); // TV上不需要角标
notificationManager.createNotificationChannel(channel);
// 发布推荐,ID必须唯一
int notificationId = RECOMMENDATION_ID_BASE + index;
notificationManager.notify(notificationId, notification);
}
}
你想想看,如果ID重复了会怎样?后发布的会把前面的覆盖掉。我之前就犯过这个错,结果用户看到的推荐永远是最后一条。嗯,排查了半天才发现是ID没处理好。
12.4 通知栏推荐与内容更新
TV上的通知栏推荐,和手机上的通知不太一样。手机通知会弹出,TV通知不会。TV的通知是直接显示在Launcher主屏的推荐行里。
内容更新这块,我建议采用“增量更新”策略。什么意思?就是不要每次把所有推荐都删了重新发,而是只更新变化的部分。
更新策略对比:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 全量更新 | 实现简单 | 闪烁、性能差 |
| 增量更新 | 流畅、省电 | 需要维护状态 |
我个人习惯用增量更新。具体做法是:维护一个推荐列表的缓存,每次更新时对比新旧列表,只发布新增的、移除过期的。
public class RecommendationUpdater {
private Set<String> currentRecommendations = new HashSet<>();
public void updateRecommendations(Context context, List<ContentItem> newItems) {
// 1. 找出需要移除的(旧列表有,新列表没有)
for (String oldId : currentRecommendations) {
if (!newItems.contains(oldId)) {
removeRecommendation(context, oldId);
}
}
// 2. 找出需要新增的(新列表有,旧列表没有)
for (ContentItem item : newItems) {
if (!currentRecommendations.contains(item.getId())) {
publishNewRecommendation(context, item);
}
}
// 3. 更新缓存
currentRecommendations.clear();
for (ContentItem item : newItems) {
currentRecommendations.add(item.getId());
}
}
private void removeRecommendation(Context context, String contentId) {
NotificationManager manager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(contentId.hashCode());
}
}
注意:
我曾经在项目里遇到一个问题:用户看了推荐内容后,返回Launcher,推荐卡片还在。这其实不对。正确的做法是:用户点击后,应该立即移除该推荐,或者标记为“已读”。
12.5 推荐内容的生命周期管理
推荐不是发出去就完事了。它有自己的生命周期。我总结了一下,大概分这几个阶段:
- 创建期:构建推荐卡片,发布到系统
- 展示期:在Launcher主屏展示,等待用户点击
- 过期期:内容过时,需要移除
- 点击期:用户点击,跳转到详情页
这里有个坑:系统不会自动帮你清理过期的推荐。你得自己维护。我一般会在App启动时,或者定时任务里,清理掉超过24小时的推荐。
public class RecommendationCleaner {
private static final long EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24小时
public void cleanExpiredRecommendations(Context context) {
SharedPreferences prefs = context.getSharedPreferences("recommendations", Context.MODE_PRIVATE);
Map<String, ?> all = prefs.getAll();
long now = System.currentTimeMillis();
NotificationManager manager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
for (Map.Entry<String, ?> entry : all.entrySet()) {
long timestamp = prefs.getLong(entry.getKey(), 0);
if (now - timestamp > EXPIRY_TIME) {
manager.cancel(entry.getKey().hashCode());
prefs.edit().remove(entry.getKey()).apply();
}
}
}
}
12.6 实战中的避坑指南
做TV推荐系统,我踩过的坑真不少。挑几个典型的说说:
- 图片加载:推荐卡片的图片必须提前加载好。不能在发布时现下载,会卡住主线程。我建议用Glide的
preload()提前加载。 - 推荐数量:系统最多显示5-6条推荐。发太多没用,反而浪费资源。我一般控制在5条以内。
- 跳转深度:用户点击推荐后,最好直接跳转到内容详情页,不要跳首页再跳详情。多一步操作,用户就流失了。
- 测试问题:TV模拟器上推荐功能可能不正常。我建议用真机测试,或者用Google的TV测试工具。
我的经验:
如果你发现推荐卡片在Launcher上不显示,先检查Notification Channel的importance。必须是 IMPORTANCE_HIGH 才行。我之前调了半天,结果发现是importance设成了 IMPORTANCE_DEFAULT。
12.7 性能优化建议
推荐系统如果做得不好,会影响整个TV的流畅度。我给出几个优化方向:
- 批量操作:多条推荐一起发布,不要一条一条发。用
notify()批量处理。 - 内存管理:Bitmap用完就回收。推荐卡片用的图片,发布后就可以释放了。
- 异步处理:所有推荐相关的操作,都放到后台线程。不要在UI线程做。
- 缓存策略:推荐列表缓存到本地,避免每次启动都重新构建。
说白了,推荐系统就是TV应用的“门面”。用户打开电视,第一眼看到的就是你的推荐。做得好,用户粘性就高。做得不好,用户连你的App都不想打开。
好了,这一章的内容就到这里。记住:推荐不是越多越好,而是要精准、及时、流畅。把握好这三点,你的TV应用就能留住用户。