12、推荐系统:Recommendations API、通知栏推荐与内容更新

说实话,做TV应用最头疼的问题之一,就是用户打开电视后不知道看什么。手机可以刷,电视不行。遥控器按来按去,用户就烦了。这时候,推荐系统就派上用场了。

Android TV的推荐系统,说白了就是帮用户“猜心思”。你猜得准,用户就留下来。猜不准,用户就切到别的App去了。今天我就把这块的实战经验掰开揉碎讲给你听。

12.1 Recommendations API 的核心机制

Android TV的推荐,不是我们自己写算法。Google已经帮我们搭好了架子。我们只需要把内容“喂”给系统,系统会帮我们展示在Launcher的主屏上。

这个机制的核心,就是 RecommendationRequestRecommendationResponse。嗯,其实更准确地说,是 Recommendations API 配合 Notification 来工作。

我刚开始接触这块时,以为要自己写推荐算法。后来发现,Google的思路是:你提供内容,系统负责展示。说白了,我们只需要做好“内容提供者”这个角色。

核心流程:

  • App 构建推荐卡片(Recommendation)
  • 通过 NotificationManager 发布到系统
  • 系统在 Launcher 主屏展示推荐内容
  • 用户点击后跳转到 App 指定页面
App 内容数据 构建推荐卡片 NotificationManager Launcher 主屏展示 用户点击跳转

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应用就能留住用户。


公众号:蓝海资料掘金营,微信deep3321