For a SliceProvider
to be useful, something need to be able to request and
display a slice served by that provider. In this chapter, we will explore what
it takes to do just that: display in your app a slice obtained from some other app’s
SliceProvider
.
Hosting slices is tied very closely to LiveData
from the Architecture Components.
In a nutshell, LiveData
is a way for you to subscribe to updates to some source
of data, akin to subscribing to an RxJava Observable
. LiveData
has the
added benefit of being aware of the Android lifecycle, so it can cleanly
unsubscribe from the data source when the hosting activity or fragment is
destroyed. You can learn more about LiveData
from CommonsWare’s book,
Android’s Architecture Components.
The
Slices/Inspector
sample project
shown in this chapter happens to be written in Kotlin.
If you are not familiar with Kotlin, focus less on the syntax shown in the examples
and more on the concepts outlined in the prose.
Just because somebody needs to host slices does not mean that you have to host slices in your app. So, why would you bother writing code to display other apps’ slices?
Depending on how the slice ecosystem evolves, there could be many good reasons to host slices. And, the good news is that for simple scenarios, hosting slices is not very difficult.
You have two main approaches for hosting a slice. Most developers will take the simple approach, using some Google-supplied library code to display and update the slice. If needed, though, you can get at the raw information about the slice and build your own custom rendered edition of that information.
In both cases, for the moment, we will assume that we know the Uri
of the
SliceProvider
. In the Inspector
sample app, to keep it simple, this value
is hard-coded in gradle.properties
to be the SamplerX
example from
the chapter on publishing slices:
SLICE_URI=content://com.commonsware.android.slice.dice.provider
You can test using the Inspector
with any other slice provider simply
by changing that line to provide the Uri
that you wish to test. That
Uri
is then hoisted into BuildConfig
via a buildConfigField
statement
in app/build.gradle
:
buildConfigField "String", "SLICE_URI", '"' + SLICE_URI + '"'
So, the app’s code can identify our SliceProvider
via BuildConfig.SLICE_URI
.
Only two classes are really needed to render a slice given the Uri
:
SliceView
, which is a widget that knows how to display a slice, andSliceLiveData
, which is a LiveData
that knows how to communicate with
a SliceProvider
, given its Uri
, and push slice data to a SliceView
The sample app has a ViewPager
with four tabs. The first three tabs each
show a slice from the provider identified by SLICE_URI
. Those tabs
each use a SliceView
, centered inside of a ConstraintLayout
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray">
<androidx.slice.widget.SliceView
android:id="@+id/slice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
A SliceView
has no intrinsic background. If you do nothing else, it will render
text and images with no background. In this layout, we set the ConstraintLayout
to have a gray background and the SliceView
to have a white background, so
you can see the bounds the SliceView
.
The sample app has a TabAdapter
serving as our PagerAdapter
for the ViewPager
.
For those first three tabs, it creates instances of a SliceFragment
:
package com.commonsware.android.slice.inspector
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.slice.widget.SliceView
class TabAdapter(fm: FragmentManager, private val titles: Array<String>) : FragmentPagerAdapter(fm) {
override fun getCount() = 4
override fun getPageTitle(position: Int) = titles[position]
override fun getItem(position: Int): Fragment =
when (position) {
0 -> SliceFragment.newInstance(SliceView.MODE_SHORTCUT)
1 -> SliceFragment.newInstance(SliceView.MODE_SMALL)
2 -> SliceFragment.newInstance(SliceView.MODE_LARGE)
else -> InspectorFragment()
}
}
The newInstance()
factory method on SliceFragment
takes a SliceView
“mode” value, indicating whether we want a shortcut, a small view, or a large
view. These modes were describe in the chapter on publishing slices.
In onCreateView()
, the SliceFragment
fills the mode into the SliceView
via setMode()
(via the mode
property syntax in Kotlin), defaulting to large mode if
for some reason we did not get a mode provided to us:
package com.commonsware.android.slice.inspector
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.slice.widget.SliceLiveData
import androidx.slice.widget.SliceView
private const val ARG_MODE = "mode"
class SliceFragment : Fragment() {
companion object {
fun newInstance(mode: Int) =
SliceFragment().apply {
arguments = Bundle().apply { putInt(ARG_MODE, mode) }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View? {
val result = inflater.inflate(R.layout.fragment_slice, container, false)
val sliceView = result.findViewById<SliceView>(R.id.slice)
sliceView.mode = arguments?.getInt(ARG_MODE, SliceView.MODE_LARGE) ?: SliceView.MODE_LARGE
SliceLiveData
.fromUri(activity!!, Uri.parse(BuildConfig.SLICE_URI))
.observe(this, sliceView)
return result
}
}
Then, we:
SLICE_URI
into an actual Uri
SliceLiveData.fromUri()
, to get a
SliceLiveData
that is configured to pull slice data from the identified providerobserve()
that SliceLiveData
, having its slice data be fed into the SliceView
And… that’s it.
At this point, SliceLiveData
and SliceView
take over to render the slice in
the requested mode:
Figure 720: Slice Inspector, Showing Shortcut Mode
Figure 721: Slice Inspector, Showing Small Mode
Figure 722: Slice Inspector, Showing Large Mode
All of the actions included in the slice are handled by SliceView
. In this case,
all of the SamplerX
actions work, each showing a Toast
with the appropriate
message.
Also, so long as you SliceLiveData
has an active observer, it will receive updates
to the slice content if the SliceProvider
publishes updates. So, for example,
if you inspect a slice that needs to perform network I/O — such as the weather
slice profiled in the chapter on publishing slices — you will
first get whatever the “loading” slice content is, then you will get the real
content. SliceLiveData
simply pushes new slices to the SliceView
, which updates
its content accordingly.
You do not need a SliceView
to display the contents of a slice, if you want
to decide how to display those contents yourself. Instead, have the SliceLiveData
route to your own code. It will hand you Slice
objects, which you can inspect
and use for setting up your own UI.
Unfortunately, this is largely undocumented.
The fourth tab in the Slice Inspector brings up a graph showing the various pieces of a slice:
Figure 723: Slice Inspector, Showing Graph of Slice Elements
This is handled by a separate InspectorFragment
, using a TreeView
from
this library.
A Slice
is a tree structure, created using the various builders inside of
the SliceProvider
.
The root Slice
will contain one or more SliceItem
objects, obtained via
the getItems()
method (or the items
property in Kotlin). Each SliceItem
has a getFormat()
method indicating what sort of item it is and what it contains:
FORMAT_TEXT
contains some text to render, which you can get via getText()
on the SliceItem
FORMAT_IMAGE
contains an image to render, which you can get via getIcon()
on the SliceItem
FORMAT_SLICE
contains a nested Slice
in the tree, which you can get via
getSlice()
and can then traverse as desiredThe sample app’s graph simply puts the format value into each of the nodes, since formats happen to be human-readable strings. A app that wanted to do its own rendering of a slice — such as a screen reader wanting to use text-to-speech — would need to make sense of what is all in these slices and how to use them.
Most such apps will leverage SliceMetadata
to help. You can wrap a Slice
in
a SliceMetadata
via the SliceMetadata.from()
method. SliceMetadata
will
give you higher-order information about an individual slice, such as:
isPermissionSlice()
, which attempts to tell you if this slice is really
a request for the user to grant permissions, instead of containing actual
contentThe Slice Inspector sample app cheats. It already knows the Uri
of the slice
provider, courtesy of that SLICE_URI
value in gradle.properties
.
In some cases, your app will be able to cheat as well. If you want to show slices
from some particular app, you can “bake” the Uri
to its provider into your
code by one means or another. You can use PackageManager
and queryContentProviders()
to determine if that provider exists, and if it does, you can arrange to display
its slices.
But what if you do not know what slice providers to use?
Eventually, the slice ecosystem will work out options for discovering slice
providers. There is already a partial mechanism in place, involving Intent
objects, but it is largely undocumented as of July 2018. Plus, it is fairly
abstract, moving the discovery question from “how do I get the Uri
?” to
“how do I create the correct Intent
?”.
Most of the focus on slices assume that the Uri
in question is tied to a
SliceProvider
.
However, there is code in Google’s Slice Viewer app
that supports slice Uri
values having http
or https
as a scheme, instead
of content
. This implies that slices might come directly from Web servers,
in addition to from local slice providers.
A slice host will not really care
about the difference, other than perhaps needing the INTERNET
permission where
it might not be required otherwise. SliceLiveData
is already asynchronous, so
whether the slice data is coming from another app on the device or is coming
from a server is an implementation detail for SliceLiveData
, not for the hosting app.
Right now, there is no documentation for how one would do this. It is entirely possible that this bit of code was from some experiments that will never be fully supported by the slice libraries. However, if you are implementing a slice host, you may wish to watch for any signs that slices are being served by Web sites — if nothing else, they would represent additional scenarios for your app’s test suite.