All Ruby on Rails Node JS Android iOS React Native Frontend

Introduction to Android Slices - Part II

In the previous post about Android Slices, we took a look at their possibilities, created our first Slice and tested it in different modes. In this article we will focus on more advanced slices which can take in-app actions and fetch data from our application. Let's take a look at Interactive and Dynamic Slices.

 Creating Interactive Slice

As we already know, Interactive Slices are used for handling different input actions. Let’s build one by ourselves. We will create a Slice which will allow us to show toast when clicked and will be able to toggle our Wi-Fi state. Let's do it!

View

First of all, we will take care of how our Slice will look like. There is nothing really special, we will have just two rows - one with “Make me a Toast” text and the second one with toggle responsible for enabling and disabling our Wi-Fi. A piece of code for building our Slice can look like this:

private fun createInteractiveSlice(context: Context, sliceUri: Uri): Slice {
return list(context, sliceUri, ListBuilder.INFINITY) {
row {
title = "Make me a Toast!"
}

row {
title = "Wi-Fi"
subtitle = "Enabled"
}
}
}

 

This code will give us Slice which looks like that:

Screenshot_1540975225

 

Of course, we are missing here a toggle, actions and dynamic subtitle which will change accordingly to the Wi-Fi state. We will add checking our WiFi state first - it won’t be really hard, we will use good old WifiManager for that.  

private fun createInteractiveSlice(context: Context, sliceUri: Uri): Slice {
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val isWifiEnabled = wifiManager.isWifiEnabled
val wifiSubtitle = if (isWifiEnabled) "Enabled" else "Disabled"

return list(context, sliceUri, ListBuilder.INFINITY) {
row {
title = "Make me a Toast!"
primaryAction = createToastAction()
}

row {
title = "Wi-Fi"
subtitle = wifiSubtitle
primaryAction = createWiFiToggleAction(isWifiEnabled)
}
}
}

If we are done with this, next thing we should do is to set up actions.

Actions

We are pretty done in terms of our Slice view. Now we will stop for a while and take a look at how actions can be handled by our application.

To be able to react to some action, we should create a Broadcast Receiver. It will be really simple because it will be responsible only for retrieving Intent and showing toast:

override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
context ?: return

if (intent.action == TOAST_ACTION) {
Toast.makeText(context, "Here is your Toast.", Toast.LENGTH_LONG).show()
}
}

companion object {
const val TOAST_ACTION = "toast_action"
}


So as you may see, it's pretty easy: when we receive an Intent we are checking if it contains proper action and then we are showing the toast.

 

Now, we are done with handling our actions in application but our Slice still won't trigger any. How can it be done? Of course, Slice API contains some convenient solution called Slice Actions. Lets jump to createToastAction() in our SliceProvider.


Slice Actions are basically wrapper for PendingIntent with some additional data:

private fun createToastAction(): SliceAction {
val intent = Intent(context, MyBroadcastReceiver::class.java).apply {
action = MyBroadcastReceiver.TOAST_ACTION
}

return SliceAction.create(
PendingIntent.getBroadcast(context, 0, intent, 0),
IconCompat.createWithResource(context, R.drawable.ic_wb_sunny_black_24dp),
ListBuilder.SMALL_IMAGE,
"Make Toast"
)
}

As you can see, when creating SliceAction, we need to provide an Pending Intent, icon and action title. And that's everything we need to do. After writing this simple piece of code we have our Slice which is displaying toast on a click. That wasn’t hard, right? Let's make our WiFi toggle action then!


Action Slices comes with support for different predefined actions. A toggle is one of them. To create it we can use another static method of SliceAction class: createToggle. That's a really convenient method which will create an action for us. We will have just to provide proper pending intent. Take a look on a code snippet below:

private fun createWiFiToggleAction(isWiFiEnabled: Boolean): SliceAction {
val intent = Intent(context, MyBroadcastReceiver::class.java).apply {
action = MyBroadcastReceiver.WIFI_TOGGLE_ACTION
}

val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
return SliceAction.createToggle(pendingIntent, "Wi-Fi Action", isWiFiEnabled)
}

Pretty straightforward, right? But how we will handle it in our broadcast receiver if we don’t have any indicator in which state our toggle is? With help comes very cool feature of Slices API: when some action is triggered, Slice will update your intent with all necessary data such as slider value, toggle state etc for you. Let’s take a look at how we can retrieve this data in our Broadcast Receiver:

if (intent.action == WIFI_TOGGLE_ACTION) {
val isWiFiTurnedOn = intent.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, false)
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager

wifiManager.isWifiEnabled = isWiFiTurnedOn
}

 

As we can see we are checking if our Intent has a proper action and then we can easily get extra information containing our toggle state by static field of Slice class. As I mentioned before, this value is added by default after action is invoked so we don’t have to worry about passing some kind of data on our own. 

Updating the view

The last thing we should do in case of Interactive Slices is updating our view. As you may see if we enable or disable our WiFi our Slice view is not being updated. What should we do with this? We can create another Broadcast Receiver which will be handling any wifi network state changes and then update our Slice. The whole process may sound a bit complicated but is really easy. Our WiFiBroadcastReceiver will look like this:

override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action
if (action == WifiManager.NETWORK_STATE_CHANGED_ACTION) {
context?.contentResolver?.notifyChange(MySliceProvider.INTERACTIVE_SLICE_URI, null)
}
}

 

Remember to include proper configuration of your WifiBroadcastReceiver into AndroidManifest.xml file!


The most important part here is to update our Slice content by invoking notifyChange method on content resolver. We just need to provide our Slice URI and after that, an application which hosts our Slice will recreate it. Now our Slice is done:

device-2018-10-25-102632_1


Looks pretty powerful, right? But that is not everything in terms of more advanced Slices. In the next section, I will cover Dynamic Slices and we will also build one by ourselves.

Creating Dynamic Slice

First, we need to know the difference between interactive and dynamic slices. Interactive Slices are used for presenting static data with actions. That means all the information displayed on the Slice are available at the moment of creating it. Dynamic Slices are used for presenting content which is frequently changing. This kind of data can be fetched from an application, modified in Slice and then returned back to an application.


We will build simple Dynamic Slice which will fetch the value from our application and will be able to modify it. Of course, the same value can be changed through an application and this change should be reflected in our Slice. Let’s do it!

View and Actions

Let's start with implementing view and actions. We need to create a simple Slice with InputRange, primary action and an input action. It will look like this:

private fun createDynamicSlice(context: Context, sliceUri: Uri): Slice {
return list(context, sliceUri, ListBuilder.INFINITY) {
inputRange {
title = "Change value"
value = 50
primaryAction = createActivityAction()
inputAction = createInputPendingIntent()
}
}
}

Our Input Action is regular PendingIntent without any actions or extras. We can create it just like this:

val intent = Intent(context, MyBroadcastReceiver::class.java)
return PendingIntent.getBroadcast(context, 0, intent, 0)

 

As you may remember from the previous section, our intent will be filled with all the information required by our Broadcast Receiver. Next step is, of course, implementing the logic of changing the value in our app. Of course, it's also done by Broadcast Receiver:

if (intent.hasExtra(Slice.EXTRA_RANGE_VALUE)) {
val value = intent.getIntExtra(Slice.EXTRA_RANGE_VALUE, -1)
if (value != -1) {
SharedPreferencesUtil.save(context, value)
}
}

So as you can see, we are using the constant from Slice class again to get extra value - our range input in this case. And it's all we need. For brevity, I will omit the whole process of saving the value into Shared Preferences, but I believe you know how to handle this.


So to sum up, now we have a Slice which can change our value and our Broadcast Receiver which can receive new value and store it in Shared Preferences. And by storing it there, we can easily use this value in our app. So let’s implement some really basic logic for our application to give user possibility to see current value and change it.

Handling value in application

In one of our Activities, we will need TextView to present current value and two buttons to increase and decrease it. Each value change should be written into Shared Preferences because our Slice will read it from there. Sample code of it will look like this:

override fun onCreate(savedInstanceState: Bundle?) {
// ...
// rest of onCreate body

minusBtn.setOnClickListener {
saveValue(getValue() - 1)
updateView()
}

plusBtn.setOnClickListener {
saveValue(getValue() + 1)
updateView()
}
}

override fun onResume() {
super.onResume()
updateView()
}

private fun saveValue(value: Int) {
SharedPreferencesUtil.save(this, value)
}

private fun getValue(): Int {
return SharedPreferencesUtil.getValue(this)
}

private fun updateView() {
valueTv.text = SharedPreferencesUtil.getValue(this).toString()
}

So now we can modify our value from two places: Slice and application. The last thing we need to do is to fetch the current value from Shared Preferences in our Slice to be sure whenever the user changes the value in the application, our Slice will be updated with it. 

Dynamic content

To do this we have to know one thing. When our Slice is created, our application is in a Strict Mode. That means we cannot perform any long running operations like network requests or reading from files, so reading a value from Shared Preferences isn’t allowed either. We will need to make it asynchronous and update our content when data is ready. That's the essence of Dynamic Slices.


For asynchronous operation, we will use Executors class for clarity. Of course, you can use any solution you want here as long as it will be executed in the background. We also will create placeholder version of our Slice to indicate that our content is being loaded - again, for simplicity, we will just use the simple row with the title.


private var currentValue = -1

private fun createDynamicSlice(context: Context, sliceUri: Uri): Slice {
fetchCurrentValueAsync()

return if (currentValue >= 0) {
list(context, sliceUri, ListBuilder.INFINITY) {
inputRange {
title = "Change value"
value = currentValue
primaryAction = createActivityAction()
inputAction = createInputPendingIntent()
}
}
} else {
list(context, sliceUri, ListBuilder.INFINITY) {
row {
title = "Loading"
primaryAction = createActivityAction()
}
}

}
}

Again, there is nothing really hard here. When creating Slice we are triggering asynchronous value fetching from Shared Preferences. Next, we have to check if our value is at least 0 - that means data was loaded. If we have our content ready we will create our Slice or simple placeholder otherwise. But what about updating content in Slice? Of course, it will be done in fetchCurrentValueAsync():

private fun fetchCurrentValueAsync() {
Executors.newSingleThreadExecutor().execute {
val context = context ?: return@execute

val value = SharedPreferencesUtil.getValue(context)
if (value != currentValue) {
currentValue = value
context.contentResolver.notifyChange(DYNAMIC_SLICE_URI, null)
}
}
}

 

As you may see, after fetching new value we are again using notifyChange to let Slice hosting application update content in our Slice. 

 

That's it, we are done with Dynamic Slice. Creating it wasn't that hard - it's a really simple case, but imagine how advanced your Slice can be! Let's see how final effect looks like:

 

ezgif-1-f4a756dae3de (1)

 

Pretty nice, isn't it?

Final Words

We created two simple but fully functional Slices. The whole process is not so hard but we need to remember a few things:


  • Slice is communicating with an application through Broadcast Receivers
  • When creating Slice our application is in Strict Mode so we can’t do any long-running operations
  • Use SliceActions static methods to create predefined actions
  • Use Slice static fields to retrieve extras from action intents

With those tips in your mind, you will be able to create rich, engaging and beautiful Slices. It will give you many new opportunities to display remote content. It's really worth to take a look and experiment by yourself.


If you want to see the whole code used in this article, check out this GitHub repository:

https://github.com/GabrielSamojlo/android-slices/tree/advanced


Happy coding!

Why it is impossible to hire quality engineers in London and how to deal with it
Code stories CTA
READ ALSO FROM Android Jetpack
Read also
Need a successful project?
Estimate project or contact us