27、Slice:Slice架构、SliceProvider、SliceBuilder、SliceAction、实战:快捷功能开发

Slice 这个东西,说实话刚出来的时候我也有点懵。它到底是什么?简单说,Slice 就是让你的 App 功能“长”到系统搜索框或者 Google App 里的一种方式。用户不用打开你的 App,就能直接操作核心功能。

我最早接触 Slice 是在做一款音乐播放器的时候。产品经理说:“能不能让用户在搜索框里直接搜歌、直接播放?” 我当时第一反应是“这得写多少代码?” 后来发现,Slice 就是干这个的。

Slice 架构:到底长什么样?

Slice 的架构其实不复杂。它本质上是一个轻量级的 UI 模板,由系统渲染,数据由你的 App 提供。

核心角色有三个:

  • SliceProvider:数据提供者,负责响应系统的 Slice 请求
  • SliceBuilder:UI 构建器,用代码搭界面
  • SliceAction:交互动作,处理点击、滑动等事件

我习惯把 Slice 理解成“App 的快捷窗口”。系统通过 URI 来请求某个 Slice,你的 SliceProvider 返回一个 Slice,系统把它渲染到搜索列表里。整个过程是异步的,不会阻塞主线程。

Slice 架构交互图 系统(System UI) 请求 Slice SliceProvider 构建 Slice SliceBuilder 返回 Slice 对象 渲染到 UI SliceAction 处理点击 回调 App 整个流程:系统请求 → Provider 构建 → Builder 组装 → Action 响应

SliceProvider:你的数据出口

SliceProvider 是 Slice 系统的入口。你需要继承它,然后重写 onBindSlice 方法。系统会传一个 Slice URI 给你,你根据这个 URI 返回对应的 Slice。

我记得第一次写的时候,踩了一个坑:SliceProvider 必须在 Manifest 里注册,而且权限声明不能少。不然系统根本找不到你的 Provider。

// AndroidManifest.xml
<provider
    android:name=".MySliceProvider"
    android:authorities="com.example.app.slices"
    android:exported="true"
    android:grantUriPermissions="true" />

注意:exported 必须为 true,否则系统无法访问你的 SliceProvider。我曾经因为这个排查了半天,最后发现是 Manifest 配置漏了。

再来看看 Provider 的实现:

public class MySliceProvider extends SliceProvider {

    @Override
    public Slice onBindSlice(Uri sliceUri) {
        // 根据 URI 的 path 判断返回哪个 Slice
        String path = sliceUri.getPath();
        if ("/quick_actions".equals(path)) {
            return createQuickActionSlice(sliceUri);
        }
        return null;
    }

    private Slice createQuickActionSlice(Uri sliceUri) {
        // 使用 SliceBuilder 构建
        ...
    }
}

你想想看,每个 URI 对应一个功能模块。比如 /quick_actions 是快捷操作,/search 是搜索。这样设计很清晰,也好维护。

SliceBuilder:搭界面就像拼积木

SliceBuilder 是真正干活的东西。它提供了一系列方法,让你用代码搭出一个 UI 模板。说白了,就是 Row、Column、Text、Icon 这些元素的组合。

我个人习惯把 SliceBuilder 的构建过程拆成三步:

  1. 创建主列表:用 ListBuilder 作为容器
  2. 添加行:用 RowBuilder 放标题、副标题、图标
  3. 绑定动作:用 SliceAction 处理点击
private Slice createQuickActionSlice(Uri sliceUri) {
    // 1. 创建 ListBuilder
    ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);

    // 2. 构建第一行:打开 App
    SliceAction openAppAction = SliceAction.create(
        PendingIntent.getActivity(getContext(), 0,
            new Intent(getContext(), MainActivity.class), 0),
        null, "打开 App"
    );

    RowBuilder row1 = new RowBuilder(listBuilder, sliceUri.buildUpon().appendPath("open").build())
        .setTitle("打开音乐 App")
        .setSubtitle("一键进入主界面")
        .setPrimaryAction(openAppAction);

    listBuilder.addRow(row1);

    // 3. 构建第二行:播放最近歌曲
    SliceAction playAction = SliceAction.create(
        PendingIntent.getService(getContext(), 1,
            new Intent(getContext(), MusicService.class)
                .setAction("PLAY_RECENT"), 0),
        null, "播放最近歌曲"
    );

    RowBuilder row2 = new RowBuilder(listBuilder, sliceUri.buildUpon().appendPath("play").build())
        .setTitle("播放最近歌曲")
        .setSubtitle("继续上次的播放列表")
        .setPrimaryAction(playAction);

    listBuilder.addRow(row2);

    return listBuilder.build();
}

小技巧:RowBuilder 的 URI 最好带上不同的 path,这样系统可以缓存不同的 Slice 状态。我之前把所有行都用同一个 URI,结果点击后状态更新出了问题。

SliceAction:让 Slice 活起来

SliceAction 就是 Slice 的“灵魂”。没有 Action 的 Slice 只是一个静态卡片,有了 Action 才能响应用户操作。

SliceAction 支持三种类型:

类型 用途 示例
Activity 跳转 打开 App 的某个页面 点击后进入播放列表
Service 启动 后台执行任务 点击后开始下载
Broadcast 发送 通知 App 更新状态 点击后切换开关

嗯,这里要注意:SliceAction 的 PendingIntent 必须唯一。如果两个 Action 用了同一个 requestCode,系统可能会混淆。我遇到过点击“播放”却打开了“设置”页面的情况,就是因为 requestCode 重复了。

// 正确的做法:每个 Action 使用不同的 requestCode
SliceAction action1 = SliceAction.create(
    PendingIntent.getActivity(context, 100, intent1, PendingIntent.FLAG_IMMUTABLE),
    null, "动作一"
);

SliceAction action2 = SliceAction.create(
    PendingIntent.getActivity(context, 200, intent2, PendingIntent.FLAG_IMMUTABLE),
    null, "动作二"
);

实战:快捷功能开发

光说不练假把式。我们做一个完整的快捷功能:用户可以在系统搜索框里直接“打开手电筒”、“打开设置”、“播放音乐”。

先定义 URI 结构:

// 基础 URI
public static final Uri BASE_URI = Uri.parse("content://com.example.app.slices/quick_actions");

// 各个功能的 URI
public static final Uri FLASHLIGHT_URI = BASE_URI.buildUpon().appendPath("flashlight").build();
public static final Uri SETTINGS_URI = BASE_URI.buildUpon().appendPath("settings").build();
public static final Uri MUSIC_URI = BASE_URI.buildUpon().appendPath("music").build();

然后在 Provider 里分发:

@Override
public Slice onBindSlice(Uri sliceUri) {
    switch (sliceUri.getPath()) {
        case "/quick_actions/flashlight":
            return buildFlashlightSlice(sliceUri);
        case "/quick_actions/settings":
            return buildSettingsSlice(sliceUri);
        case "/quick_actions/music":
            return buildMusicSlice(sliceUri);
        default:
            return buildMainSlice(sliceUri); // 返回主菜单
    }
}

构建手电筒 Slice:

private Slice buildFlashlightSlice(Uri sliceUri) {
    ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);

    // 开关动作
    SliceAction toggleAction = SliceAction.create(
        PendingIntent.getBroadcast(getContext(), 300,
            new Intent("com.example.action.TOGGLE_FLASHLIGHT"), PendingIntent.FLAG_IMMUTABLE),
        null, "切换手电筒"
    );

    RowBuilder row = new RowBuilder(listBuilder, sliceUri)
        .setTitle("手电筒")
        .setSubtitle("点击开关")
        .setPrimaryAction(toggleAction);

    listBuilder.addRow(row);
    return listBuilder.build();
}

关键点:这里用了 Broadcast 而不是 Activity,因为开关手电筒不需要打开界面。后台处理就行,用户体验更好。

最后别忘了在 Manifest 里声明 Slice 的 URI 权限:

<provider
    android:name=".QuickActionSliceProvider"
    android:authorities="com.example.app.slices"
    android:exported="true"
    android:grantUriPermissions="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.app.slice.category.SLICE" />
    </intent-filter>
</provider>

做完这些,你的快捷功能就上线了。用户在系统搜索框里搜你的 App 名,就能看到这些 Slice,点击直接操作。

说实话,Slice 的坑不少。比如不同厂商的系统对 Slice 的支持程度不一样,有的直接不显示。我的建议是:把 Slice 当作锦上添花的功能,不要依赖它做核心交互。但一旦用好了,用户体验的提升是实打实的。

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