Blog

Exploring Android P’s slices: Creating interactive and dynamic slices

Category: AndroidAuthority
28 0

The hard work isn’t over just because you’ve successfully released your app and built up a user base. Once you’ve found your audience, you need to hang onto them!

At this year’s I/O, Google announced Android slices, a new feature to help keep users engaged with your application. Android slices appear in places where many Android users spend a lot of time, including Google search results, so they’re an effective way to keep users coming back to your application.

By the end of this article, you’ll have created two slices: a simple slice that launches an Activity, and a dynamic slice that lets users interact with your app, from outside of the application context.

What are Android slices?

Android Slices are snippets of your app’s content displayed outside of your application. They will be debuting in Google search, and Google plans to add slice support to other applications and areas of the operating system in the future.

Slices can display a range of content, including text, images, video, live data, scrolling content, and deep links, as well as interactive controls such as toggles and sliders. Slices can also be dynamic, updating to reflect events happening inside your application.

Imagine you’ve installed an app for booking tickets for your local cinema. The next time you’re Googling the latest blockbuster, you’ll get the usual search results, and maybe that application’s “Book Now” slice. This lets you reserve tickets to see this movie at your local cinema, without having to navigate away from your search results.

From the user’s perspective, this slice has provided them with quick and easy access to the feature they needed at that exact moment. From the developer’s perspective, this slice got their application in front of the user in a relevant context, and successfully re-engaged them.

Android Slices are also part of Android Jetpack, so they’re supported on everything from Android 4.4 onwards. If you add slices to your project, according to Google the slices have the potential to reach 95 percent of all Android users!

Create your first slice

Slices can perform a range of actions, but let’s keep things simple for now and create a slice that launches our application’s MainActivity.

Start by creating a new project using the latest canary build of Android Studio 3.2, then open your project’s build.gradle file and add the androidx.slice dependencies. To keep things consistent, I’m also using the AndroidX namespace for the other dependencies.

dependencies {
 implementation fileTree(dir: 'libs', include: ['*.jar'])
 implementation 'androidx.appcompat:appcompat:1.0.0-alpha1'
 implementation 'androidx.constraintlayout:constraintlayout:1.1.0'
 implementation 'androidx.slice:slice-core:1.0.0-alpha2'
 implementation 'androidx.slice:slice-builders:1.0.0-alpha2'
 testImplementation 'junit:junit:4.12'
 androidTestImplementation 'androidx.test:runner:1.1.0-alpha1'
 androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha1'
}

At the time of writing, the process of creating a slice sometimes caused Android Studio to automatically add duplicate slice-core and slice-builders dependencies. If you’re encountering strange error messages, check your build.gradle file to make sure this hasn’t happened.

Create your slice provider

A slice provider is the component that lets you display slices outside of your application, including in Google search results.

To create a slice provider:

  • Control-click your project’s “src” package, got to New… > Other > Slice Provider.
  • Name this slice provider “MySliceProvider.”
  • Click “Finish.”

Every time a host application needs to display a slice, it’ll send a binding request to your slice provider, with the Uniform Resource Identifier (URI) of the slice it wants to display. The slice provider will then call onCreateSliceProvider() and build the slice by calling the onBindSlice() method. Finally, the onBindSlice() method will return the slice and pass it to the host application.

If you open your MySliceProvider class, the automatically generated code provides an overview of this process:

import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.ListBuilder.RowBuilder;

//Create a class that extends SliceProvider//

public class MySliceProvider extends SliceProvider {

//Initialize your slice provider, by calling onCreateSliceProvider//

 @Override
 public boolean onCreateSliceProvider() {
 return true;
 }

 @Override
 @NonNull
 public Uri onMapIntentToUri(@Nullable Intent intent) {
 Uri.Builder uriBuilder = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT);
 if (intent == null) return uriBuilder.build();
 Uri data = intent.getData();
 if (data != null && data.getPath() != null) {
 String path = data.getPath().replace("/", "");
 uriBuilder = uriBuilder.path(path);
 }
 Context context = getContext();
 if (context != null) {
 uriBuilder = uriBuilder.authority(context.getPackageName());
 }
 return uriBuilder.build();
 }

//Build the slice//

 public Slice onBindSlice(Uri sliceUri) {
 Context context = getContext();
 if (context == null) {
 return null;
 }

//Check the URI path//

 if (sliceUri.getPath().equals("/")) {

//Create a ListBuilder, which you’ll use to add rows to your slice//

 return new ListBuilder(getContext(), sliceUri)

//Construct your rows using RowBuilder, and then add them to the list//

 .addRow(new RowBuilder(context, sliceUri).setTitle("URI found."))

//Build the list//

 .build();
 } else {
 return new ListBuilder(context, sliceUri)
 .addRow(new RowBuilder(context, sliceUri).setTitle("URI not found."))
 .build();
 }
 }

 @Override

//Note that we don’t cover pinning a slice in this article//

 public void onSlicePinned(Uri sliceUri) {

//Register any observers that need to be notified of changes to the slice’s data//

 }

 @Override
 public void onSliceUnpinned(Uri sliceUri) {

//Don’t forget to unregister any observers to avoid memory leaks//

 }
}

Since SliceProvider is a content provider, it has to be declared in your project’s Manifest. When you create a slice provider using Android Studio by going to New… > Other > Slice Provider, this declaration gets added to your Manifest automatically:

</activity>

 <provider
 android:name=".MySliceProvider"
 android:authorities="com.jessicathornsby.launchslice"
 android:exported="true">
 <intent-filter>
 <action android:name="android.intent.action.VIEW" />

 <category android:name="android.app.slice.category.SLICE" />

 <data
 android:host="jessicathornsby.com"
 android:pathPrefix="/"
 android:scheme="http" />
 </intent-filter>
 </provider>
 </application>

</manifest>

Making your Android slices interactive: Creating a Slice Action

If this Android slice is going to launch our application’s MainActivity, we need to make some changes to the slice provider:

Define a SliceAction

You make a slice interactive by creating one or more slice actions. A SliceAction can consist of a title, an icon, and a PendingIntent, which handles user interaction in your slices.

I’m going to define a single slice action to launch our application’s MainActivity.

 public SliceAction createActivityAction() {
 Intent intent = new Intent(getContext(), MainActivity.class);
 return new SliceAction(PendingIntent.getActivity(getContext(), 0, intent, 0),
 IconCompat.createWithResource(getContext(), R.drawable.ic_home),
 "Launch MainActivity");
 }

Then, I’m going to mark this as the slice’s primary action, so it’ll trigger whenever the user interacts with any part of the slice:

 public Slice createSlice(Uri sliceUri) {
 SliceAction activityAction = createActivityAction();
…
…
…
 .setPrimaryAction(activityAction);

Define the slice’s content

Although you can customize your Android slices to a degree, ultimately they’re templated content. You cannot precisely position a slice’s UI elements like when defining an application’s layout via XML files.

To build a slice’s user interface, you need to implement a ListBuilder, specify the type of rows you want to display, and define the content for each row.

For now, let’s keep things simple and use a basic RowBuilder, which supports all of the following content types:

  • A title item. This appears at the start of the row. The title item can be a timestamp, an image, or a SliceAction.
  • A title. This is a single line of text, formatted as a title.
  • A subtitle. This is a single line of text, formatted as regular text.
  • A start item. This can be an icon, a timestamp, or a SliceAction.
  • End items. These are items that appear at the end of each row. You can supply multiple end items for each row, but depending on the available space some of these end items may not be displayed on certain devices. Your start and end items can either be a timestamp, an icon, or a SliceAction.
  • A primary action. This is the action that’ll trigger whenever the user taps the row.

To keep things simple, I’m going to create a single row, consisting of a “Launch MainActivity” title.

import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;

import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;

public class MySliceProvider extends SliceProvider {

 @Override
 public boolean onCreateSliceProvider() {

 return true;
 }

 @Override
 public Slice onBindSlice(Uri sliceUri) {
 final String path = sliceUri.getPath();
 switch (path) {

//Define the slice’s URI; I’m using ‘mainActivity’//

 case "/mainActivity":
 return createSlice(sliceUri);
 }
 return null;
 }

 public Slice createSlice(Uri sliceUri) {
 SliceAction activityAction = createActivityAction();

//Create the ListBuilder//

 ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);

//Create the RowBuilder//

 ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder(listBuilder)

//Set the title text//

 .setTitle("Launch MainActivity.")

//Set the row’s primary action//

 .setPrimaryAction(activityAction);

//Add the row to the ListBuilder//

 listBuilder.addRow(rowBuilder);

//Build the List//

 return listBuilder.build();
 }

 public SliceAction createActivityAction() {
 Intent intent = new Intent(getContext(), MainActivity.class);
 return new SliceAction(PendingIntent.getActivity(getContext(), 0, intent, 0),
 IconCompat.createWithResource(getContext(), R.drawable.ic_home),
 "Launch MainActivity");
 }

}

This is all you need to create a functioning slice. However, since slices are still an experimental feature, you’ll need to jump through a few hoops before you can experience this slice in action.

Testing Android slices with the Slice Viewer

At the time of writing, you can only test your Android slices using Google’s Slice Viewer application, which emulates how slices will eventually appear in Google search results.

To install Slice Viewer:

  • Make sure your Android device is attached to your development machine, or that your Android Virtual Device (AVD) is up and running.
  • Download the Slice Viewer app.
  • Move the Slice Viewer APK to your Android/sdk/platform-tools folder.
  • Open a Command Prompt (Windows) or Terminal (Mac).
  • Change directory (“cd”), so the window is pointing at your Android/sdk/platform-tools folder, like this:

cd /Users/jessicathornsby/Library/Android/sdk/platform-tools

  • Install the Slice Viewer APK on your Android device or AVD, by typing the following command into the Command Prompt or Terminal window, and then pressing the Enter key:

./adb install -r -t slice-viewer.apk

Next, you’ll need to create a slice run configuration, and pass it your slice’s unique URI:

  • Go to Run > Edit Configurations… from the Android Studio toolbar.
  • Click the little “+” icon and then select “Android App.”

  • Enter “slice” into the Name field.
  • Open the ‘Module’ dropdown, and then select ‘app.’
  • Open the “Launch” dropdown, and select “URL.”
  • Next, enter your slice’s URL, in the format slice-content://package-name/slice-URL. For example, my slice’s URL is:

slice-content://com.jessicathornsby.launchslice/mainActivity

  • Click OK.
  • Select Run > Run slice from the Android Studio toolbar, and select your device.

This app will now be installed on your Android device. Slice Viewer will request permission to access your app’s slices; tap Allow and your slice should appear onscreen.

Give the slice’s “Launch MainActivity” button a click, and the slice should respond by launching your application’s MainActivity.

Download the finished application from GitHub.

Creating a dynamic slice

Let’s move onto something more exciting and create a dynamic slice, which allows users to interact with the related application directly from the slice’s user interface.

This second application is going to display a value the user can increase and decrease, either from the application itself, or from the slice. Regardless of whether the user changes the value in the app or the slice, the new data will be synced across both components, so they’ll always have access to the latest data.

To build this slice, either create a new project, or update your existing application. If you do decide to create a fresh project, then you’ll need to repeat the following setup:

  • Create a MySliceProvider class, by control-clicking your project’s “src” folder and selecting New… > Other > Slice Provider.
  • Add the following dependencies to your build.gradle file:
dependencies {
 implementation fileTree(dir: 'libs', include: ['*.jar'])
 implementation 'androidx.appcompat:appcompat:1.0.0-alpha1'
 implementation 'androidx.constraintlayout:constraintlayout:1.1.0'
 implementation 'androidx.annotation:annotation:1.0.0-alpha1'
 implementation 'androidx.slice:slice-core:1.0.0-alpha2'
 implementation 'androidx.slice:slice-builders:1.0.0-alpha2'
 testImplementation 'junit:junit:4.12'
 androidTestImplementation 'androidx.test:runner:1.1.0-alpha2'
 androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha2'
}

Create the application layout

Start by creating the application’s user interface.

Open your project’s activity_main.xml file, and create an “Increase” and a “Decrease” button, plus a TextView to eventually display the application’s dynamic value:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical"
 tools:context=".MainActivity">

 <TextView
 android:id="@+id/click_count"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:textSize="20sp"
 android:padding="20dp"/>

 <Button
 android:id="@+id/increase"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="+1"/>

 <Button
 android:id="@+id/decrease"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="-1"/>

</LinearLayout>

We also need to create a string resource that’ll display our dynamic value:

<resources>
 <string name="app_name">dynamicSlice</string>
 <string name="click_string">Count: %d\u00B</string>
</resources>

Creating vectors with the Vector Asset Studio

In the slice, I’m going to display “Up” and “Down” arrows that change the application’s value when tapped:

  • Control-click your project’s “res” directory and select New > Vector Asset.
  • Click the little “Clip Art” icon.
  • Select the “Arrow upward” resource, and then click OK.
  • Give your asset the name “ic_count_up,” and then click Next.
  • Click Finish.

Repeat the above steps, but this time select the ‘Arrow downward’ icon and give it the name “ic_count_down.”

Updating a slice at runtime

Every time the user increases or decreases the value, we need to make sure our slice knows about it!

To inform a slice about changes, our app needs to call context.getResolver.notifyChange(Uri, null), which will trigger the onBindSlice() method and cause the slice to be rebuilt with the new content.

import android.os.Bundle;
import android.content.Context;
import android.widget.TextView;
import android.net.Uri;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;
import androidx.annotation.NonNull;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

 public static int clickCount = 0;

 private TextView mTextView;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);

 mTextView = findViewById(R.id.click_count);

 findViewById(R.id.increase).setOnClickListener(this);
 findViewById(R.id.decrease).setOnClickListener(this);
 }

 @Override
 public void onClick(View view) {
 int id = view.getId();
 switch (id) {
 case R.id.increase:

//Increase the value//

 updateClickCount(getApplicationContext(), clickCount + 1);
 break;
 case R.id.decrease:

//Decrease the value//

 updateClickCount(getApplicationContext(), clickCount - 1);
 break;
 }
 mTextView.setText(getClickString(getApplicationContext()));
 }

 public static String getClickString(@NonNull Context context) {
 return context.getString(R.string.click_string, clickCount);
 }

 public static void updateClickCount(Context context, int newValue) {

 if (newValue != clickCount) {
 clickCount = newValue;

//Retrieve the URI that’s mapped to this slice//

 Uri uri = MySliceProvider.getUri(context, "clickCount");

//Notify the slice about the updated content//

 context.getContentResolver().notifyChange(uri, null);
 }
 }
}

Creating a multi-choice slice

In our second slice provider, we need to complete the usual steps (such as implementing onCreateSliceProvider and onBindSlice), plus the following:

  • Create multiple SliceActions. We need to define separate slice actions for when the user increases the value, and when they decrease the value.
  • Handle user input. We’ll also need to define a PendingIntent to register our app’s value change events. In the next step, we’ll be creating a BroadcastReceiver to handle these PendingIntents.
  • Supply some end items. You can display timestamps, icons, and slice actions at the end of each row. I’m going to use the “Up” and “Down” vectors as my slice’s end items.

Here’s the finished MySliceProvider class:

import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.app.PendingIntent;
import android.net.Uri;

import androidx.slice.builders.ListBuilder;
import androidx.slice.Slice;
import androidx.slice.builders.SliceAction;
import androidx.slice.SliceProvider;

import androidx.core.graphics.drawable.IconCompat;

import static com.jessicathornsby.dynamicslice.MyBroadcastReceiver.ACTION_CHANGE_COUNT;
import static com.jessicathornsby.dynamicslice.MyBroadcastReceiver.EXTRA_COUNT_VALUE;
import static com.jessicathornsby.dynamicslice.MainActivity.getClickString;
import static com.jessicathornsby.dynamicslice.MainActivity.clickCount;

public class MySliceProvider extends SliceProvider {
 private Context context;
 private static int count = 0;

 @Override
 public boolean onCreateSliceProvider() {
 context = getContext();
 return true;
 }

 @Override
 public Slice onBindSlice(Uri sliceUri) {
 final String path = sliceUri.getPath();
 switch (path) {

//Define the URI//

 case "/clickCount":
 return createClickSlice(sliceUri);
 }
 return null;
 }

 private Slice createClickSlice(Uri sliceUri) {

//Define two SliceActions//

 SliceAction clickUp = new SliceAction(getChangeCountIntent(clickCount + 1),
 IconCompat.createWithResource(context, R.drawable.ic_count_up).toIcon(),
 "Increase count");
 SliceAction clickDown = new SliceAction(getChangeCountIntent(clickCount - 1),
 IconCompat.createWithResource(context, R.drawable.ic_count_down).toIcon(),
 "Decrease count");

 ListBuilder listBuilder = new ListBuilder(context, sliceUri);
 ListBuilder.RowBuilder clickRow = new ListBuilder.RowBuilder(listBuilder);

 clickRow.setTitle(getClickString(context));

//Add the actions that’ll appear at the end of the row//

 clickRow.addEndItem(clickDown);
 clickRow.addEndItem(clickUp);

//Add the row to the parent ListBuilder//

 listBuilder.addRow(clickRow);

//Build the slice//

 return listBuilder.build();
 }

//Define the PendingIntent that’ll eventually trigger our broadcast receiver//

 private PendingIntent getChangeCountIntent(int value) {
 Intent intent = new Intent(ACTION_CHANGE_COUNT);
 intent.setClass(context, MyBroadcastReceiver.class);
 intent.putExtra(EXTRA_COUNT_VALUE, value);
 return PendingIntent.getBroadcast(getContext(), count++, intent,

//If the PendingIntent already exists, then update it with the new data//

 PendingIntent.FLAG_UPDATE_CURRENT);
 }

 public static Uri getUri(Context context, String path) {
 return new Uri.Builder()
 .scheme(ContentResolver.SCHEME_CONTENT)
 .authority(context.getPackageName())
 .appendPath(path)
 .build();
 }
}

Handling the slice’s intents

Finally, we need to create the broadcast receiver for retrieving each new value, and informing the slice provider whenever it needs to rebuild the slice:

  • Control-click your project’s “src” folder and select New > Other > Broadcast Receiver.
  • Enter the name “MyBroadcastReceiver,” and then click Finish.
  • Open your MyBroadcastReceiver file, and add the following:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import static com.jessicathornsby.dynamicslice.MainActivity.clickCount;
import static com.jessicathornsby.dynamicslice.MainActivity.updateClickCount;

public class MyBroadcastReceiver extends BroadcastReceiver {

 public static String ACTION_CHANGE_COUNT = "com.jessicathornsby.slicetesting.ACTION_CHANGE_COUNT";
 public static String EXTRA_COUNT_VALUE = "com.jessicathornsby.slicetesting.EXTRA_COUNT_VALUE";

 @Override
 public void onReceive(Context context, Intent intent) {
 String action = intent.getAction();

 if (ACTION_CHANGE_COUNT.equals(action) && intent.getExtras() != null) {

//Retrieve the new value//

 int newValue = intent.getExtras().getInt(EXTRA_COUNT_VALUE, clickCount);
 updateClickCount(context, newValue);
 }
 }

}

Put your dynamic slice to the test

To test this slice, you’ll need to create a second run configuration that passes this particular slice’s unique URI:

  • Select Run > Edit Configurations from the Android Studio toolbar.
  • Click the little “+” icon and select “Android App.”
  • Give this configuration a name.
  • Open the “Launch” dropdown, and then select “URL.”
  • Enter the URI for triggering this slice. I’m using the following:

slice-content://com.jessicathornsby.dynamicslice/clickCount

  • Click “OK.”
  • Select Run > Run slice from the Android Studio toolbar.

Your slice will now appear in the emulator or connected Android device.

To put this slice to the test, tap its “Up” and “Down” arrows, and switch to your application’s MainActivity. Tap either of the application’s “Increase” or “Decrease” buttons, and it should start counting from the value you created in the slice, rather than from zero. If you switch back to the slice, you should find the value has updated automatically.

Download the complete project from GitHub.

Wrapping up

Now you know how to implement this new feature. Will you be using slices in your own Android projects? Let us know in the comments below!

Leave a comment