Introduction
App Widgets can provide a huge amount of value to users by surfacing relevant information outside of an app. They are still not as common as we’d expect, so we’re revisiting app widgets on Android to see what’s changed in the new Jetpack Glance framework, that will help developers like us create App Widgets quickly and more easily than ever before!
We wanted to create a meaningful widget that went beyond Hello World or a couple of buttons. In this journey, many blogs we found helped along the way, but there is some conflicting information due to development approaches — traditional XML layout based, and Glance / Compose based. So, this article will try to clarify some of those as well as give a broader perspective of User Experience (UX) and discoverability of App Widgets (which has been challenging in the past).
Benefits of App Widgets and Glance Framework
App Widgets are often under-utilized in the Android Ecosystem. We traditionally see App Widgets only on system apps like Calendar, Email, or simpler apps like weather apps. From a User Experience (UX) and marketing standpoint, they can offer a lot. App Widgets are supposed to be simple, yet provide just enough information to peak interest. Here are some benefits:
Increases user engagement : Travel, Health, Retail apps can show upcoming reservations, appointments, specials / offers.
Less invasive and convenient : Some users may disable notifications. App Widget simply exist on the home screen and can be visible passively on the home screen and user will likely tap to engage the full app experience.
It is great to see a renewed push by Google with App Widget enhancements with the Android 12 release. There can be multiple App Widget instances associated with one app. Under the hood, App Widgets rely on RemoteViews which allows rendering views on another process. Our understanding is that Home Screen is a process that implements AppWidgetHost and we are loaning some parts of our app functionality to that process. The biggest advantage of the Glance framework is that it takes a lot of boilerplate code out by generating RemoteViews from Glance Composable code.
Full version of partial code snippets in this article can be accessed at: https://gist.github.com/harishrpatel/adcac2edfa930351c7789ede59214a7e
Features of Our App Widget
For this article, we will refer to a personal Calendar App called Consolidator that renders days with different colors based on types of events. We need to show either one month or two months depending on space on home screen and provide UP/DOWN arrow buttons for navigation. In order to explore communication between App and App Widget, we will fetch events (holidays, recurring meetings, etc.) data every 15 minutes and refresh App Widget.
Figure 1. App Widget Demo Video
App Widget Architecture
Glance based App Widget implementation requires very little code, so we will just describe how different components interact. As illustrated below, four major aspects of our implementation are:
UI / Glance Composable code
WorkManager to periodically fetch calendar data
DataStore / SharedPreferences to help cache events data
AppWidgetManager to facilitate interactions with these components
Figure 2. App Widget Architecture Diagram
Here is one flow: WorkManager fetches calendar entries, caches data in DataStore and informs AppWidgetManager to update / refresh the widget. Now there is disconnect, which is why we cache data in DataStore. AppWidgetManager now invokes Glance Composable code which will read DataStore cache and render the App Widget UI.
We need to implement ActionCallback interface to handle button click actions. We update the month when the user clicks UP/DOWN buttons and take the user to the app when they click on calendar (to get detailed information). GlanceAppWidgetReceiver is an implementation of AppWidgetProvider that returns life cycle callbacks that we really did not interact with.
Design Considerations
As described above, Glance reduces quite a bit of code and provides enough for most widgets. This table shows some criteria that one should consider before starting design.
Table 1. Design Considerations
Discoverability
Advanced users are likely to find App Widgets and install them. But now there is a programmatic way to prompt users to add App Widget by using requestPinAppWidget method in one of the App’s Activity or Fragment. This call should be wrapped around checks to not annoy users with frequent popups if user has already added an App Widget. AppWidgetManager methods to get providers do not accurately distinguish if user has added an App Widget. One can store a flag in DataStore in the requestPinAppWidget callback to get around this.
Figure 3. Prompting User to Add App Widget
User Interface
Glance implementation does not use XML layouts except for a preview layout (starting from Android 12 — API 31). If using WorkManager, set updatePeriodMillis to 0 indicating that WorkManager will update App Widget. Here is an example of xml-v31/calendar_widget_info.xml file. Default version will look identical except for a couple of attributes.
<!-- API 31 (Android 12 and up) --> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/widget_description" android:minWidth="200dp" android:minHeight="200dp" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/calendar_widget_preview_image" android:previewLayout="@layout/calendar_widget_small_layout" android:targetCellWidth="2" android:targetCellHeight="3" android:maxResizeWidth="300dp" android:maxResizeHeight="600dp" android:widgetCategory="home_screen" android:updatePeriodMillis="0" />
While most of the supported UI components worked well, there were some difficulties with LazyColumn. We avoided these by using Column instead. Composable code is translated into RemoteViews, so nested lists or very complex UI should be avoided. Glance Compose layout parameters are stricter than Jetpack Compose ones but can easily be fixed by tweaking GlanceModifiers. Here is an example of a Composable:
Column( modifier = GlanceModifier.padding(all = 2.dp).fillMaxWidth().background(Color.White), horizontalAlignment = Alignment.CenterHorizontally ) { // UP/Prev button Image( provider = ImageProvider(com.bottlerocket.android.consolidator.R.drawable.ic_arrow_up_24), contentDescription = "Back", modifier = GlanceModifier.defaultWeight() .background(Color.Transparent) .clickable(onClick = actionRunCallback<CalendarActionCallback>( actionParametersOf(pairs = arrayOf(ActionParameters.Key<DateArrow>(KEY_DATE_ARROW) to DateArrow.Back))) ) ) val monthEvents = calendarEventsMap.filter { it.key.year == firstMonthState.year && it.key.month == firstMonthState.month } MonthCalendarWidgetUI(firstMonthState, monthEvents) if (numMonthsToShow >= 2) { val secondMonth = firstMonthState.plusMonths(1) val secondMonthEvents = calendarEventsMap.filter { it.key.year == secondMonth.year && it.key.month == secondMonth.month } MonthCalendarWidgetUI(secondMonth, secondMonthEvents) } }
Data and Periodic Refresh with Work Manager
We chose Work Manager to refresh App Widget at 15 minutes intervals (a WorkManager minimum period). WorkManager’s ability to pass in constraints for battery optimization and integration with Android system and Glance framework were primary reasons. Using PeriodicWorkRequestBuilder ’s setInitialDelay method really helped giving few seconds to initialize the App Widget before starting to fetch data. Also make sure to pass ExistingPeriodicWorkPolicy.KEEP ensuring only a single instance of Workmanager request is queued.
fun startCalendarDataFetch(workManager: WorkManager) { calendarWidgetLog( "queueCalendarDataRequest" ) val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging( false ) .setRequiresBatteryNotLow( true ) .build() // NOTE: min period is 15 minutes. val request = PeriodicWorkRequestBuilder<CalendarWorkManager>( Duration.ofMinutes( CALENDAR_WORK_REQ_PERIOD_MINUTES )) .setConstraints(constraints) .setInitialDelay(Duration.ofSeconds(CALENDAR_WORK_REQ_INITIAL_DELAY_SECONDS)) .build() // MUST: Specifying "unique" and "KEEP" will ensure there is only one worker in the system. val operation = workManager.enqueueUniquePeriodicWork( CALENDAR_FETCH_WORKER, ExistingPeriodicWorkPolicy.KEEP, request) calendarWidgetLog( "WorkManager queue status: ${operation.result}" ) }
The biggest advantage of WorkManager is that it reduced activity at night when our test device was in Doze mode for battery optimization. When the device was being charged overnight, a 15-minute Work Manager thread polled at that cadence all night. But when not plugged in, it only polled at 12:30 AM, 1:30 AM, 3:30 AM and then 6:30 AM to avoid unnecessary work.
Challenges
Even though Glance (as of writing this BLOG) is v1.0.0-Alpha-05 , we feel it allowed us to complete our App Widget. We sincerely hope that Glance creators will resolve some bugs and transition to a released version.
Code to periodically update App Widget was not smooth, but it got the job done. It would have been nice if GlanceAppWidgetManager had an update() method that would refresh all installed App Widget instances instead of updating via GlanceAppWidget class.
private suspend fun updateWidgetData(events: List<DayEvent>) { GlanceAppWidgetManager(context = context).getGlanceIds(ConsolidatorAppWidget:: class .java).forEach { glanceId -> updateAppWidgetState(context, glanceId) { prefs -> calendarWidgetLog( "updateAppWidgetState: glanceId: $glanceId" ) // Each app widget instance stores in a different DataStore file. prefs.setCalendarData(events) } // VERY IMPORTANT: Refreshing App Widget with updated events here. calendarWidgetLog( "updateWidgetData: Update UI" ) ConsolidatorAppWidget().update(context, glanceId) } }
There is no easy way to detect if a user has installed App Widget; none of the callbacks from App Widget components gave this indication. So, one must start WorkManager request when user launches app. Luckily WorkManager runs in the background even after the app instance is swiped off.
Debugging and Testing:
Use logcat statements to debug synchronization issues with Data Store, Work Manager, app and App Widget. WorkManager’s minimum repeat interval is 15 minutes, so one must find efficient ways to test.
All the Widget code is part of the application, so one can attach debugger and add breakpoints as usual.
Glance tries to make callbacks for every size variant for dynamic resizing. To reduce clutter while debugging non-UI code, have only one widget size for debugging (as described below).
class ConsolidatorAppWidget : GlanceAppWidget () { private val isDebugOverride = false // Set to true to have only one widget size to reduce logging clutter /** * Used to dynamically configure Widget as user is resizing it. */ override val sizeMode: SizeMode = SizeMode.Responsive( if (isDebugOverride && isDebug()) setOf(ONE_MONTH_WIDGET) else setOf(ONE_MONTH_WIDGET, TWO_MONTH_WIDGET) ) ... }
Conclusion
App Widgets promote user engagement and Glance makes development fun while creating a professional experience quickly. As Glance is in its Alpha release phase currently, they are very receptive to get feedback . Glance Composable code provides a stable way to design UI. Data persistence and refresh inconsistencies add a few challenges, but they can be overcome. WorkManager familiarity or finding other ways to refresh data should help. In short, keep the first App Widget relatively simple and grow gradually. Starting with Android 12, emphasis on discoverability of a widget has been improved, and allows for much more visibility with users.
Useful Links