CounterWidget
app, which demonstrates how to build an increment counter widget. We will cover how to use Glance to structure the UI, manage state using Jetpack’s DataStore, and handle interactions like button clicks. The CounterWidget
example is a great starting point for understanding the fundamentals of creating app widgets using modern Android development practices.In this explanation, we will walk through the code step-by-step, beginning with the widget’s layout and composable functions, followed by how the widget’s state is updated, and how actions such as button clicks are handled. By the end, you'll have a clear understanding of the necessary components involved in creating a fully functional app widget with Kotlin and Glance.
Structure of the CounterWidget
The CounterWidget
class extends GlanceAppWidget
, which is the core component responsible for rendering the widget's content. It overrides the Content()
composable function to define the layout and behavior of the widget. This layout includes a text view displaying the current count and a button to increment the counter.
A Column
is used to organize the layout vertically, centering the elements both horizontally and vertically. The background color for the widget is set to a light blue (0xFFD3E9FA
), and the GlanceModifier.fillMaxSize()
ensures the widget fills the available space. The count is displayed in the Text
composable using a large font size (50.sp) and is aligned to the center. The button labeled "Up" allows the user to increment the counter when pressed. Its size is set to 100x50 dp, with a background color of 0xFFB6C0C9
.
Managing State with Preferences
One of the key features of this app widget is its ability to retain the counter value across widget updates. To achieve this, Jetpack’s DataStore is used to store the counter state persistently. The countPreferenceKey
is defined as a key to access the stored counter value, and it is retrieved using currentState<Preferences>()
. If the key is not already present (e.g., the widget is used for the first time), a default value of 0 is provided.
The GlanceStateDefinition
is overridden to use PreferencesGlanceStateDefinition
, which binds the widget to DataStore’s state management. This allows the counter value to be retrieved and updated in a persistent manner without needing a backend service.
Handling Button Clicks and State Updates
The button's functionality is handled using the actionRunCallback<UpdateActionCallback>()
, which triggers an action when the button is pressed. This is where the counter's value is incremented. The parameters passed to the callback include the updated count value (count + 1
).
The UpdateActionCallback
class implements the ActionCallback
interface. The onRun()
function, which gets executed when the button is pressed, retrieves the new count from the action parameters. It then updates the widget’s state by calling updateAppWidgetState()
. This function writes the new counter value to DataStore by converting the preferences into a mutable form and assigning the updated count. Finally, CounterWidget().update()
refreshes the widget's UI to display the new count.
Widget Configuration and Manifest Setup
The widget’s appearance and size are defined in the res/xml/counter_widget_info.xml
file. Here, the minWidth
and minHeight
attributes determine the default size of the widget, while resizeMode
allows users to resize it both horizontally and vertically. Additionally, the widget is marked as a home screen widget with android:widgetCategory="home_screen"
.
In the AndroidManifest.xml
, the CounterWidgetReceiver
class is declared as a broadcast receiver that listens for app widget update events. It specifies that the receiver handles the APPWIDGET_UPDATE
action. This ensures that the system knows when to update the widget, especially after a configuration change or an action-triggered update. The receiver is also linked to the widget’s XML configuration through the <meta-data>
tag.
Dependencies
Lastly, in the build.gradle
file, the Glance library is added as a dependency with the following line:
This includes all the necessary APIs to create, manage, and update app widgets using Jetpack Compose.
Summary
In this article, we explored how to create a simple counter app widget using Kotlin and Jetpack Compose’s Glance framework. The CounterWidget
uses a combination of DataStore for state persistence and Glance composables to build an interactive user interface directly on the home screen. We walked through how the UI is structured, how the state is managed and updated, and how interactions like button clicks are handled.
This example demonstrates the potential of using modern Compose tools to build efficient and functional widgets with ease. By utilizing Glance, developers can leverage the power of Jetpack Compose, even outside the app’s main interface, creating a more consistent and enjoyable user experience.
package com.cfsuman.widgetexamples
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.glance.*
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.layout.*
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
private val countPreferenceKey = intPreferencesKey("count-key")
private val countParamKey = ActionParameters.Key<Int>("count-key")
class CounterWidget : GlanceAppWidget(){
override val stateDefinition: GlanceStateDefinition<*> =
PreferencesGlanceStateDefinition
@Composable
override fun Content(){
val prefs = currentState<Preferences>()
val count = prefs[countPreferenceKey] ?: 0
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
modifier = GlanceModifier
.background(Color(0xFFD3E9FA))
.fillMaxSize()
) {
Text(
text = count.toString(),
modifier = GlanceModifier.fillMaxWidth(),
style = TextStyle(
textAlign = TextAlign.Center,
color = ColorProvider(Color.Blue),
fontSize = 50.sp
)
)
Spacer(modifier = GlanceModifier.padding(8.dp))
Button(
text = "Up",
modifier = GlanceModifier
.background(Color(0xFFB6C0C9))
.size(100.dp,50.dp),
onClick = actionRunCallback<UpdateActionCallback>(
parameters = actionParametersOf(
countParamKey to (count + 1)
)
)
)
}
}
}
class UpdateActionCallback : ActionCallback{
override suspend fun onRun(context: Context, glanceId: GlanceId,
parameters: ActionParameters) {
val count = requireNotNull(parameters[countParamKey])
updateAppWidgetState(
context = context,
definition = PreferencesGlanceStateDefinition,
glanceId = glanceId
){ preferences ->
preferences.toMutablePreferences()
.apply {
this[countPreferenceKey] = count
}
}
CounterWidget().update(context,glanceId)
}
}
class CounterWidgetReceiver : GlanceAppWidgetReceiver(){
override val glanceAppWidget:GlanceAppWidget = CounterWidget()
}
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:minWidth="100dp"
android:minHeight="50dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:widgetCategory="home_screen"
/>
<receiver
android:name=".CounterWidgetReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false">
<intent-filter>
<action android:name
="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/counter_widget_info" />
</receiver>
implementation "androidx.glance:glance-appwidget:1.0.0-alpha03"
- jetpack compose - Kotlinx serialization build json array
- jetpack compose - Flow current time
- jetpack compose - How to flow a list
- jetpack compose - How to use ViewModel state
- jetpack compose - Flow using ViewModel
- jetpack compose - Search Room data using ViewModel
- jetpack compose - ViewModel Room add insert data
- jetpack compose - ViewModel Room edit update data
- jetpack compose - ViewModel Room delete clear data
- jetpack compose - Icon from vector resource
- jetpack compose - IconButton from vector resource