android

Android P Slices: the missing documentation — part 2

“It depends” 🤷‍♂️ — Google Developer Expert for Android, Flutter and Identity. A geek 🤓 who has a serious thing for good design ✨ and for emojis 🤟

We've been looking in-depth into what Android P Slices are and how you can write an app to host them. In the second part of this series we'll be exploring the other side of the coin and see what a Slice is made of, and how to create a SliceProvider to expose your Slices to other apps.

In the first part of this series we have seen what Slices are, what is their possible use, and how you can host slices from other apps into your own. If you haven't done it yet, you should go read the first part of the series, as this article assumes you're familiar with its contents:

Where do Slices come from?

As we have seen in the first part of this series, slices are emitted by an app that contains a SliceProvider.

Roles in the slices system

A SliceProvider is nothing but a good old ContentProvider, although you would never be able to tell by its API:

public abstract class SliceProvider extends ContentProviderWrapper {

  public void attachInfo(Context context, ProviderInfo info) { /* ... */ }

  public abstract boolean onCreateSliceProvider();

  public abstract Slice onBindSlice(Uri sliceUri);

  public void onSlicePinned(Uri sliceUri) {
  }

  public void onSliceUnpinned(Uri sliceUri) {
  }

  public @NonNull Uri onMapIntentToUri(Intent intent) {
      throw new UnsupportedOperationException(
              "This provider has not implemented intent to uri mapping");
  }

  // ...
}

A SliceProvider implementer only needs to concern itself with emitting slices and dealing with pinning and unpinning, whereas the underlying code will take care of handling URI permissions, ensuring the caller package has received permission from the user to show slices from it, etc.

Since it's a content provider, a SliceProvider has to be declared in the AndroidManifest.xml as such:

<provider
    android:name="com.android.mypkg.MySliceProvider"
    android:authorities="com.android.mypkg" />

Note that according to the documentation, the provider's authority is supposed to match the application package in which the provider is located, although I am not sure if that is a strict requirement or is just a recommendation. The code in the slices-core compat package doesn't seem to enforce any such requirement, in any case.

You can also add an <intent-filter> to the manifest entry for the provider if you want host apps to be able to bind to slices by intent in addition to by URI; in that case you need to override onMapIntentToUri() in your SliceProvider implementation, and provide the mapping from intents to URIs.

How it's made: slices

The simplest scenario is the one in which the host app has the user's permission to host slices from a provider and it calls SliceManager.bindSlice():

Binding a slice is a well-defined flow

In this case, the SliceProvider will initialise itself, call onCreateSliceProvider() to let the implementation initialise itself, and then immediately call onBindSlice(). This method is tasked with returning a Slice for the provided URI, which is then passed to the host application.

As mentioned earlier, all other implementation details are hidden away from a provider implementer: enforcing URI permissions, verifying that the user has given explicit permissions to the host app to show a certain other app's slices, and so on. Provider implementers have thus no control over things like the appearance of the slice(s) that is used to request the user permission. That special slice is instead created by the slice framework, either in androidx.slice.compat.SliceProviderCompat (if running on a pre-P device) or in android.app.slice.SliceProvider if slices are natively supported by the OS.

If a permission request is involved, the flow becomes more convoluted

As you can see from the flow diagram above, if the host app is using bindSlice() it will have to re-call it once the permission has been granted. There is no API for checking whether a user has granted the permission, unfortunately. At this point, the permission check will succeed in the framework, and the provider's onBindSlice() will be called, emitting the actual content that will then be passed to the host app.

Pinning slices

In addition to the basic scenario where slices are directly bound to, there is a more interesting scenario in which a slice is pinned by a host on a provider. Pinning a slice only means that to indicate to a provider that there are one or more hosts that are interested in the data of a slice, and that it should notify of updates to the underlying data using ContentResolver.notifyChange() in a timely manner.

Pinning a slice does not mean that you get updates to its data, as slices are static snapshots, but rather only signals the interest into a specific slice to its provider, so that it can for example subscribe to underlying data changes on its side, and then publish fresh data when observed via a SliceLiveData or a callback.

This in practice means that providers that want to support pinning need to implement onSlicePinned() and onSliceUnpinned() and, in there, register/unregister observers to the data which makes up the pinned slice, so that when it changes it can notify host apps of the updates. The slices framework will transparently take care of re-binding updated slices on the host side and calling the registered listener(s) — or LiveData<Slice>.

As mentioned previously, pinning is implicit on the host side whenever a listener is registered, or when using SliceLiveData to observe a slice. Since pinning does not survive reboots, hosts are in charge of persisting their pinned slices however they see fit and re-pin them on BOOT_COMPLETED, or whenever they need to.

In the current sources for the compat slices libraries there are several TODOs in this area, including one pointing at providing a notifyChange(Uri, Slice) which should simplify things considerably both for developers and for the framework.

Loading content for slices

A SliceProvider needs to return a slice as quickly as possible; this means that any blocking disk or network I/O to create a slice is strictly forbidden. Providers should whenever possible keep a memory cache of their pinned slices so that they can serve them quickly.

If a slice contains data that needs to be loaded from disk or from the network, it should omit it from the initial slice and instead mark each pending item as isLoading (all setters have overloads for it) and begin loading it in the background. Once the loading is completed, it should re-compile the slice and notifyChange() so that hosts can request a new copy of the slice with all the data.

Anatomy of a Slice

The innards of a slice

Slices are static tree-like data structures. Everything inside of a slice is either a SliceItem, or a property (hints, specs, …). When a (sub)slice is constructed internally, it can be assigned:

  • A SliceSpec which says what kind of slice it is (currently, it's BASIC, LIST or MESSAGING)
  • A series of "hints", which are strings describing slice properties and subtypes, so that the renderers know what to inflate and how to handle it
  • A series of subitems, stored as a list of SliceItems
    • SliceItems can have a series of hints just like slices
    • SliceItems have a format, expressed as a SliceType, which is one of: FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT, FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT
    • Slice items can have a subtype which provides additional data beyond the SliceItem's format
    • All contents of a slice except for hints and specs are slice items
    • A slice has a series of convenience "typed" adders for subitems (actions, text, etc), which are internally represented as a SliceItem with a specific FORMAT_* type

When it comes to creating a slice, there is no direct API on Slice itself. Instead, a slice can be constructed only from a TemplateSliceBuilder, an abstract class that only has two concrete "top-level" implementations: ListBuilder and GridBuilder.

There is another implementation, MessagingSliceBuilder, but that is restricted to the support library group and is not accessible to users of the support libraries.

Screenshots of the sample application

If you look at the Slice&Dice sample app on GitHub, you will find an example of both a host and a provider apps; they both provide an example of what can be currently achieved with slices, with a bunch of workarounds for present issues and sample code for three different slices.

It all starts with a list

The most common scenario, and the one you’ll find in the sample app, is using a ListBuilder to create a slice that has one or more rows of content. A list can contain several kinds of items:

A list slice and its components

If you want to have a look at how all the various items are rendered, you can grab the demo app and select the Demo slice.

"Regular" list row

A "regular" list row represents the most plain and common item type in a slice list. They are composed of:

A regular row

  • A title item
    • A title item can be: a timestamp, an icon, or an action
  • A title and a subtitle text
  • An end item
    • An end item can be: a timestamp, an icon, or an action
  • A primary action (triggered when clicking the row)

All icons in a "regular" row are forcefully tinted when rendered in a host, as mentioned in the previous part of the series, so you should only use monochromatic images.

Header row

A header row is a specialised version of a "regular" row, and all lists have one. You can setHeaderRow() explicitly, but if you don't, the first item in the list will become a header, regardless. A header can have:

A header row

  • A title and a subtitle text
  • A summary subtitle — replaces the regular subtitle when rendered in small mode
  • A primary action — displayed as an end action (headers are not clickable)

And they appear to all be optional, although it may just be that the HeaderBuilder implementation used under the hood is lacking proper validation.

Header rows look much like regular rows (they end up both being instances of RowView when inflated in a SliceView), but they have slightly larger text, and they don't have title or end items. Headers are not supposed to scroll away, and they should be representative of the activity the data originates from.

All icons in a header row are forcefully tinted when rendered in a host, as mentioned in the previous part of the series, so you should only use monochromatic images.

Grid row

A grid row is the most complex type of row as it contains a set of cells (with a maximum of 5). They cells will not wrap on multiple lines and will not become a carousel if there are too many of them. Grid rows contain:

A grid row

  • A set of cells
    • The maximum number of cells is 5, all subsequent ones are ignored when rendering
    • A cell contains:
      • An image
      • A title text and a "normal" text — defined as just text
      • A content intent, which is the same of an action but with a different name — not sure why this is
    • The cell image has one of these ImageModes: LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE
      • Icon images are tinted and small
  • A "see more" cell, or a “see more” action
    • You can't have them both
    • The "see more" cell counts towards the maximum of 5 cells, although it's not documented
    • The "see more" cell looks and works exactly like any other cell, so I am not entirely sure why you would need one
  • A primary action

The way a grid is rendered depends a lot on the contents. The height of the items in the row, in particular, depends on a few factors:

Main condition Secondary condition Tertiary condition Cell height
All cells have only images Only one cell Mode is SMALL 120dp
Mode is not SMALL 140dp
More than one cell All images are icons 60dp
Images are not all icons 86dp
Cells don't only have images Cell has two text lines and mode is not SMALL Cell has image 140dp
Cell has no image 60dp
Cell has less than two text lines or mode is SMALL All images are icons 60dp
Images are not all icons 120dp

Range row

A range row is a specialised kind of row, which has a progress bar to represent a value in a range. A Range row contains:

A range row

  • A title text
  • A maximum value — the minimum is fixed at zero just like with a ProgressBar
  • A value, which must be in the [0, maximum value] interval

Input Range row

An Input Range row is the interactive counterpart to a Range row, which has a SeekBar instead of a ProgressBar. An Item Range row has:

An input range row

  • A title text
  • A maximum value — the minimum is fixed at zero just like with a ProgressBar
  • A value, which must be in the [0, maximum value] interval
  • An action which is triggered whenever the user changes the SeekBar value
    • There is currently a bug for which this is invoked whenever assigning a slice that contains the Input Range to a SliceView, which makes the Input Range unusable (#75004842)

See More row/See More action

A See More row is just a "regular" row with an additional hint marking it as See More. There is currently no difference in rendering or handling for See More rows from any other "regular" row. As an alternative to a See More row you can specify a See More action, which is wrapped in a subslice under the hood.

Action

A list slice can have one or more actions associated with it. An action has:

  • A PendingIntent, which is the actual action wrapped by the object
  • An icon, with a content description
  • A title
  • Two flags:
    • Toggle — to signify that this action is an on/off switch
    • Checked — used with Toggle, sets the status of the action
  • A priority — a positive integer where the lowest priority ranks highest in sorting

Actions are shown:

  • In an "actions row" when the view is in large or small mode and there are two or more actions
    • Actions in the action row are shows as clickable images, optionally tinted, no text
    • Actions are supposed to be sorted by priority but, as of the alpha1 implementation, they're not (yet)
    • The actions row is supposed to be shown at the bottom of the slice contents
  • At the bottom of the header row if the view is in large mode

At least, that's the theory. In testing, actions are not shown in any of the three modes (#74889520).

What I think is still missing

While slices are an exciting new territory to explore, I feel like there's many things they still need to get in their API and implementation to be truly useful and not just a pipe dream, hindered by a restrictive implementation.

The main missing opportunity I can see is the ability to provide "public" slices — slices that every app can attach to without having to ask for permissions to do so. If the slice doesn't expose any sensitive data or behaviour, I don't really see a reason to require an explicit user permission to bind to it. For example, a weather app that likely doesn't expose any private information through a "London weather" slice shouldn't require a permission to provide that slice. On the other hand, a "Weather at the user's location" might require a permission since it involves the position permission. It's a tricky one to handle, granted, but the potential payoff is great. (#76021782)

The APIs need some polishing too, in particular for Kotlin users. The current builder APIs don't validate the contents of slices, allowing providers to craft slices which can make a host app crash. (#76021783). Besides, while the presence of a consumer-based API for builders is welcome, a Kotlin-friendly API which simplifies the slice creation would be very welcome on top of the regular Java one. (#76031961)

On the rendering and customizability side, I would like to see a few things in the next previews:

  • Improved grids support (#74889524, #76021784)
    • Allow to control the style and size of cells
    • Allow to have more than two lines of text
    • Allow more than 5 items, and show as carousel if they don't fit on one line
    • Visually distinguish the "see more" cells from normal cells
  • Allow slices to specify their items' background colours, or at least, define styles that can be overridden for slice items on the SliceView side (#74917012)
  • Make list headers more visually distinct from normal rows (#74917014)
  • Allow for images in start/end items not to be tinted (#76031962)
  • Add new types of slice items (#76021785)
    • E.g., an item that only contains an image
  • Allow slice items to define their Gravity (#76031963)
    • E.g., should an item be start-, or end-aligned? Or should it fill all the row?
  • Allow action listeners to hijack actions, not just to listen to them but also to handle things by themselves (#76031964)
    • E.g., if the app uses Custom Tabs, it should be able to display web content there instead of in the system browser

Conclusions

Slices are an extremely interesting new API, full of potential. Google keeping mum about the feature and not saying much about it seems to hint to them sandbagging in anticipation for some big reveal in DP2, which should drop during Google I/O in early May. It's still an immature API and its implementation is quite buggy, but those are things that are bound to get sorted out before the final P release.

Having a compatibility library also means the API can evolve separately from the system's implementation, like Fragments do nowadays. In fact, one might wonder why even bothering to release a platform version of slices, but there might be some piece of the puzzle that is still missing to make sense of it.

The inaccessible Messaging slice is very intriguing, as one can imagine scenarios in which messaging chats can be integrated in other apps. For example, imagine a helpdesk SDK or Facebook messenger providing you with a way to integrate their services in your app directly.

I can envision, with some tweaks to slices, that in the future apps will be offering all sorts of integrated UI to other apps. A social sign-in button, a cart and checkout for e-commerce, and integrations into Assistant are just a few of the many things one could do with Slices, assuming they evolved a bit.

If you are curious about slices and want to get your hands dirty, head over to the Slice&Dice sample app repository and start playing around with the code!


Many thanks once again to all those that helped me with this post!

Enjoyed this article? There's more...

We send out a small, valuable newsletter with the best stories, app design & development resources every month.

No spam, no giving your data away, unsubscribe anytime.

About Novoda

We plan, design, and develop the world’s most desirable software products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact