Lesson 07 / 14
Widgets
A widget is a small view that lives on the home screen and holds data updates until the widget is opened. The lesson measures that of the accumulated updates only the last one is drawn, and that updates do not reach a widget that has been removed from the home screen.
Published Updated
In this lesson
- Separates the widget provider, the update request and the layout
- Measures that of the updates accumulated until the widget opens, only the last one is drawn
- Shows that the updates of two widgets alive at the same time are superseded according to their keys
- Shows that updates sent to a widget removed from the home screen are lost
Contents
In the previous lesson the event came from outside and the screen could process it one more time. In a widget the update is born inside the app, but the view that will draw it lives on the home screen, outside the app’s own process. When the app updates the data, the view may not be drawn at that moment. In this lesson we ask the same question again: how many copies of this sent event reached the receiver?
// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)
class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
val copies: Int = received.count { it.seq == sent.seq }
val verdict: String = when {
copies > 1 -> "duplicated"
copies == 1 -> "once"
received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
else -> "lost"
}
fun line() {
println("$label: key=${sent.key} | copies=$copies -> $verdict")
}
}
fun countArrivals(arrivals: List<Arrival>) {
for (name in listOf("once", "duplicated", "superseded", "lost")) {
println("$name: ${arrivals.count { it.verdict == name }}")
}
}
The classification is the same as in the previous lessons. The rule counts the copies in the receiver’s list that carry the same sequence number; if there is no copy, it checks whether a later event with the same key arrived. It does not see the content or the cause.
The Provider and the Layout
A widget has three parts. The definition in the manifest introduces the widget to the platform, the layout says how the view is drawn, and the widget provider receives the update requests. The provider is called when the platform asks for an update, and it gives the new view back to the platform.
// Platform code: WeatherWidget
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
class WeatherWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, manager: AppWidgetManager, ids: IntArray) {
for (id in ids) {
val views = RemoteViews(context.packageName, R.layout.widget_weather)
views.setTextViewText(R.id.temperature, latestTemperature())
manager.updateAppWidget(id, views)
}
}
}
onUpdate writes no number: the refresh interval is given in the definition of the widget, and the platform treats this interval as a request. This snippet claims no output or timing. The model builds this rule of the platform; the model is not the platform itself.
The postponement of an update is not a separate delivery. In M12/K04 the question was whether a job ran or was postponed; in this course, if a postponed update finally arrives it is once, and if it does not arrive it is lost. The silent channel in M12/K01 is not a loss either: a notification on a silent channel arrives and is once, it only makes no sound.
The Refresh Interval and Drawing
A refresh interval is written in the definition of the widget, but this interval is a request. To save battery, the platform can lengthen the interval on its own, and it pulls an interval given as very short up to a certain lower limit. The app does not have to settle for this interval: when the data changes it can trigger the update itself. The difference between the two ways is who keeps the clock. In a widget refreshed by interval the platform keeps the clock, and the view waits for the next interval even if the data has changed. In an update triggered by the app the clock is in the app, and the widget is refreshed the moment the data changes.
Its relation to delivery is this: the home screen does not always draw the widget. If the widget is not visible, for example because it is on another home screen page or the screen is off, the platform only stores the view. When the widget becomes visible, the last stored view is drawn. The views in between are not drawn, and a later view that takes the place of the same widget means superseded. This is the platform counterpart of the model’s rule that “of the updates accumulated until the widget opens, only the last one is drawn.” For this reason it is wrong to expect every intermediate state to be shown in a widget; the goal is to show the correct state when it becomes visible.
The Last Update Drawn When the Widget Opens
In the model each data update is a Signal, and its key is the name of the widget. When the widget is not open, the app gives the updates one after another. When the widget opens, only the last state is drawn; each update that comes while it is open is drawn one by one. The step at which the widget opens is a made-up value of the model.
// Drawn.kt — of the updates accumulated until the widget opens, only the last of each key is drawn
fun drawn(sent: List<Signal>, openAt: Int): List<Signal> {
val before = sent.filter { it.seq < openAt }
val waiting = before.filter { s -> before.none { it.key == s.key && it.seq > s.seq } }
return waiting + sent.filter { it.seq >= openAt }
}
// Main.kt — of the updates accumulated until the widget opens, only the last is drawn
fun main() {
val sent = (1..6).map { Signal("widget.weather", it) }
val received = drawn(sent, openAt = 4)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [3, 4, 5, 6] event-1: key=widget.weather | copies=0 -> superseded event-2: key=widget.weather | copies=0 -> superseded event-3: key=widget.weather | copies=1 -> once event-4: key=widget.weather | copies=1 -> once event-5: key=widget.weather | copies=1 -> once event-6: key=widget.weather | copies=1 -> once once: 4 duplicated: 0 superseded: 2 lost: 0
This lesson’s delivery is superseded. The widget opened at step 4. Updates 1 and 2 were not drawn, because update 3 with the same key took their place. Updates 4, 5 and 6, which came while the widget was open, each arrived once.
SC1. The model counts all the updates before the opening as a single waiting set. On the platform the refresh of a widget depends on the platform’s own timing, and how long the waiting updates are held is not in the model.
Two Widgets
If two widgets live on the home screen, the key of each belongs to itself. The update of one does not take the place of the other; superseding happens only among updates with the same key.
// Main.kt — with two widgets, superseding happens only within the same key
fun main() {
val sent = listOf(
Signal("widget.weather", 7),
Signal("widget.calendar", 8),
Signal("widget.weather", 9),
Signal("widget.calendar", 10),
Signal("widget.weather", 11),
Signal("widget.calendar", 12),
)
val received = drawn(sent, openAt = 10)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [8, 9, 10, 11, 12] event-7: key=widget.weather | copies=0 -> superseded event-8: key=widget.calendar | copies=1 -> once event-9: key=widget.weather | copies=1 -> once event-10: key=widget.calendar | copies=1 -> once event-11: key=widget.weather | copies=1 -> once event-12: key=widget.calendar | copies=1 -> once once: 5 duplicated: 0 superseded: 1 lost: 0
The widget opened at step 10. Of the three updates before the opening, only the weather update 7 was superseded: update 9 took its place. The calendar update 8 arrived once, because there was no other update with the same key.
SC2. The model builds the two widgets independently of each other. On the platform the same provider can serve more than one widget, and which instance an update goes to depends on its identifier number; this is not in the model.
The Configuration Screen
Some widgets ask the user for a setting: which city’s weather, which note to show. When the user adds the widget to the home screen, the platform first opens the configuration screen of the app and holds the widget until the user completes the setting. Updates that come before the configuration is finished have no setting to draw, and they drop. In the model below, the widget that shows a note is configured at step 23, while the weather widget is ready from the start.
// Configured.kt — drops updates that come before the configuration is finished
fun configured(sent: List<Signal>, key: String, doneAt: Int): List<Signal> =
sent.filter { it.key != key || it.seq >= doneAt }
// Main.kt — updates that come before the configuration is finished are not drawn
fun main() {
val sent = listOf(
Signal("widget.note", 19),
Signal("widget.weather", 20),
Signal("widget.note", 21),
Signal("widget.weather", 22),
Signal("widget.note", 23),
)
val received = configured(sent, key = "widget.note", doneAt = 23)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [20, 22, 23] event-19: key=widget.note | copies=0 -> superseded event-20: key=widget.weather | copies=1 -> once event-21: key=widget.note | copies=0 -> superseded event-22: key=widget.weather | copies=1 -> once event-23: key=widget.note | copies=1 -> once once: 3 duplicated: 0 superseded: 2 lost: 0
The two updates of the weather widget arrived once. Updates 19 and 21 of the note widget were not drawn, because the configuration finished at step 23 and update 23, which came with the same key, took their place. The rule counted these as superseded; from the user’s point of view, however, they were updates sent to a widget that had no setting yet, and so they carried no meaning at all. If no update with the same key had come after the configuration finished, the same two updates would have been counted as lost.
SC3. The model assumes the configuration finishes in a single step and for certain. On the platform the user can give up the configuration screen, and in that case the widget is never added; also, how the platform treats update requests while the configuration screen is open is not in the model.
Why Every Update Carries the Whole View
A widget is drawn not in the process of the app but in the process of the home screen app. The app cannot change the view directly; instead it produces a recipe for the view, and the platform passes this recipe to the home screen. The recipe is usually given not as the change of a single field but as the new form of the whole view. As a result each update replaces the previous one completely.
This is the natural place of the superseded delivery in this lesson. Two updates cannot be merged, because the second update is not added on top of the first, it takes its place. Between a first view containing a temperature value and a second view containing a humidity value, it is not possible to keep the first and add the second. If the second view does not contain the temperature, the temperature becomes invisible in the widget. For this reason every update must carry the whole state to be shown.
The same rule tells the app this too: the superseding of an update is not a loss of information, as long as the next update carries the whole state. The case where information is lost is the one where the next update carries only the changed field. The model does not tell these apart; it counts every update as a sequence with the same key and says superseded even if the successor carries something incomplete. Whether the successor is complete is a design decision of the app.
What Happens When the Size Changes
When the user resizes the widget, the platform tells the provider the new size and expects a new view from the app. This call is an update request and does not depend on the interval; it comes whenever the user changes the size. If the app does not produce a recipe suited to the new size, the old recipe may not fit the new size: the text is cut off, the image overflows, or the widget looks empty.
This is a second source of the superseded delivery. The update from the interval and the update from the size carry the same key, because both are the state of the same widget. If an update from the interval comes right after the size change, only the last of the two is drawn. If the last one does not know the new size, the user sees that the resizing had no effect. For this reason every recipe must be produced according to the current size; an app that reads the size once and stores it uses the old size in later updates.
Common Misconceptions
“The refresh interval is a promise.” The interval is a request. The platform can lengthen it, and if the widget is not visible it only stores the view. An app that expects every update to be drawn finds superseded updates, as in the first block of the measurement.
“A superseded update is an error.” For a widget the last state is worth more than the old one. An intermediate state that is not drawn is something the user would never see, and it shows the design, not a loss. The error is the case where the last state is never drawn: that is a loss.
“A postponed update is a separate delivery.” This course has four deliveries. A postponed update is once if it arrives, superseded if it is replaced by the next update, and lost if it never arrives. Postponement is not a type of delivery; it is a property of the time of arrival.
“Sending updates to a removed widget is harmless.” The update does no harm but it is in vain: there is no view left to draw. To stop these updates, which spend battery and reach nowhere, the app must listen for the removal call.
“A widget cannot be updated while the app is closed.” The provider works regardless of whether the app is in the foreground. The platform wakes the provider for the update, and when the work is done the process can go back to sleep. For this reason the data needed for the update must be ready in the call of the provider or must be obtainable quickly.
“Every widget shows the same view.” Two widgets added from the same provider can have different sizes and different settings; each has its own key and its own sequence of updates.
The Bounding Measurement
When the user removes the widget from the home screen, the provider is no longer a receiver of updates. The app keeps producing updates, but there is no view left to draw.
// Removed.kt — updates do not reach a widget removed from the home screen
fun removed(drawn: List<Signal>, removedAt: Int): List<Signal> =
drawn.filter { it.seq < removedAt }
// Main.kt — updates do not reach a widget removed from the home screen
fun main() {
val sent = (13..18).map { Signal("widget.weather", it) }
val received = removed(drawn(sent, openAt = 15), removedAt = 17)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [14, 15, 16] event-13: key=widget.weather | copies=0 -> superseded event-14: key=widget.weather | copies=1 -> once event-15: key=widget.weather | copies=1 -> once event-16: key=widget.weather | copies=1 -> once event-17: key=widget.weather | copies=0 -> lost event-18: key=widget.weather | copies=0 -> lost once: 3 duplicated: 0 superseded: 1 lost: 2
Update 13 was superseded, and 14, 15 and 16 arrived. Because the widget was removed before step 17, updates 17 and 18 were lost: no update with the same key arrived after them. The rule does not see that the cause of the loss is the removed widget; to know this, the app must separately ask which widget identifiers are alive.
SC4. The model builds the removal as immediate and in a single step. On the platform, the delete call for a removed widget reaches the provider, and the app can stop the updates at that point; this is not in the model.
Summary
- This lesson’s delivery: superseded. Of the updates accumulated until the widget opens, only the last of each key is drawn.
- Of six updates, two were superseded and four arrived once: superseded 2, once 4.
- With two widgets, superseding happened only within the same key: one update was superseded and five arrived once.
- Two updates were lost to the widget removed from the home screen: lost 2.
- A postponed update is not a separate category; a notification on a silent channel arrives and is once.
- Two updates of the widget whose configuration was not finished were counted as superseded because of the update that came with the same key after the end: once 3, superseded 2.
Next Step
With this lesson we have reached the end of the system capabilities: the notification, cloud messaging, location, camera, sensors, the deep link and the widget were measured with the same question. The next topic discusses the app’s own data and keys: the lesson Encrypted Storage and the Keystore deals with whose hands the stored data and the key can fall into.
To keep your progress and take notes, Log in
My notes
Log in to take notes.