Movies Widget
Widget Declaration
Widgets allow you to embed small parts of your application into a user's home screen. They can display data from your application, allow the user to interact with it, and act as customized launch points for your application.
Glance is a Jetpack Compose framework for creating and managing widgets, built on top of the existing AppWidget framework. It looks and feels quite a bit like the Compose UIs you've built before, but there's a really important difference: the UI is remote.
Your widget runs in a separate process from your application, inside the user's selected Home application. You can't directly communicate with it, nor can you add your code to that Home application. So how does it work?
In the AppWidget framework, you'd create Remote Views. They defined a limited subset of available UI views, and acted as a proxy for your user interface. The remote views are defined in a Broadcast Receiver that you define, that accepts system broadcasts describing the current state of the widget and whether it wants an update.
Broadcast Receivers are top-level Android Components, like Activities, and are declared
in the AndroidManifest.xml
file. When a broadcast Intent
is sent to them, the system
creates an instance of them, passes the Intent
to its onReceive()
function to perform its
action, then the receiver's work is done.
You create a subclass of AppWidgetReceiver
(or for Compose, GlanceAppWidgetReceiver
) to
supply the remote views for the widget. When the user adds or removes a widget for their
Home app, or after an amount of time that the widget declaration states it should be updated,
a broadcast Intent
is sent to your widget receiver. You create or update the remote views
which are sent back to the widget framework to render for the user (or remove form the Home app).
This is easier to see as we create a new widget. We'll define a widget that displays our movie list and allows the user to click on one, taking them to that movie in the application.
Setting up the widget
First we need to add the glance dependencies
- The version in the version catalog
- The libraries in the version catalog
- The dependencies in the app's build script
We'll put our code in a subpackage named com.androidbyexample.movie.glance
. To do this,
right-click on the com.androidbyexample.movie
package and choose New > package
then add ".glance" at the end and press Enter:
Note
A "subpackage" in Kotlin (or Java) is one where another package has the same
prefix. For example, a.b.c
is a subpackage of a.b
.
Aside from the name, there is no relation between these packages. If you import a.b.*
,
it doesn't make types from a.b.c
available.
Warning
Never use import-on-demand (imports like a.b.*
). See
Import on Demand is EVIL!
for details.
Info
Nested boxes are cool. So are Fezzes and Bowties.
(I'm in a mood today...)
Now we create some new code in that package.
The MovieAppWidget
class defines your user interface
for the widget. We'll just create a Text
for it.
Caution
The composable functions for Glance, like Text
have the same names as the ones we've
been using, but are in a different package. In this case, the Glance Text
is
androidx.glance.text.Text
. Be careful when adding them; be sure to choose the one from
the Glance package.
Now the MovieAppWidgetReceiver
. This is the broadcast
receiver that receives the system broadcasts about the widget to add/remove/update it. All
we need to do is define its glanceAppWidget
property.
Note
We won't be doing anything fancy with widgets (and there's much more you can do). If you want to do more, see Jetpack Glance and App widgets overview.
We need to describe to Android how our widget should behave when placed into a Home app,
including sizing information and how often to explicitly update it. This information
is specified in
app/src/main/res/xml/movie_app_widget_info.xml
.
There are many more options you can specify in the widget info file.
We attach the widget info when
registering our receiver in
app/src/main/AndroidManifest.xml
. The manifest is our way of telling Android which top-level
components we've defined in our application. So far, we've only had a single Activity in there
and haven't really talked about it.
When an app is installed, Android looks at the manifest to determine what's available. The
<intent-filter>
s specify which Intent
s this application can handle, and which components
to send those intents to. Any application can send intents to start activities or services,
and send messages to broadcast receivers. (There are some limitations but we won't go into that
detail here).
A little more on intents and filters
Let's take a quick diversion to explain in a bit more detail how Intent
s and intent-filter
s work.
When the system sees
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
The MAIN/LAUNCHER
combination of action
and category
is special - it specifies an Activity
as an entry point to the application that can be listed in the Home app to start the application.
If we had something like (untested - I haven't done this in a while...)
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
<data android:scheme="http" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
You'd be telling android that you can handle requests to view a URI that starts with "http" or "https" - something like this might be used if you were creating a web browser application. If another application sent an implicit intent that said "hey Android, I want to view https://androidbyexample.com", your application would be a candidate to handle it.
If only one app is installed that has an intent filter that matches, that application is used to perform the request. If more than one app is installed that can handle it, the user is asked which to use.
An application can send an explicit intent, which indicates which application and activity/service should handle it. We'll see this later when we talk about services.
Using the Widget
To use the widget we need to install the application. Note that we don't need to actually run the application from Android Studio, so if you'd prefer, you can edit the application launcher setting by clicking the "..." next to the app launcher on the Android Studio toolbar and choosing "Edit":
And choose "Nothing" in the Launch dropdown:
You don't need to do this, but if you don't, you'll need to return to the home screen when the application opens so you can add the widget.
To set up the widget:
- Run the application to install it.
- If you didn't change the launcher to not launch the app, go to the home screen. You can do this by clicking the "O" icon on the emulator toolbar, or swiping up from the bottom of the emulator or device and quickly releasing. The gesture can be tricky to use on the emulator, so I recommend the toolbar.
- Long press on an empty spot on the home screen. Note that this might be different based on the Home app that you're using.
- Choose Widgets
- Scroll down to the MovieWidget app
- Long press on the widget to add to the home screen (some apps define multiple widgets)
- Drag the widget to the homescreen position you'd like and release
- To remove, long press on the widget and drag to "Remove". Note that this might be different based on the Home app that you're using.
Code Changes
CHANGED: /app/build.gradle.kts
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinAndroid) } kotlin { jvmToolchain(17) } android { namespace = "com.androidbyexample.movie" compileSdk = 34 defaultConfig { applicationId = "com.androidbyexample.moviewidget" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { implementation(project(":repository"))implementation(libs.glance.appwidget) implementation(libs.glance.material3)// implementation(libs.icons.extended) implementation(libs.lifecycle.compose) implementation(libs.core.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.activity.compose) implementation(platform(libs.compose.bom)) implementation(libs.ui) implementation(libs.ui.graphics) implementation(libs.ui.tooling.preview) implementation(libs.material3) testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.espresso.core) androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(libs.ui.test.junit4) debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.test.manifest) }
CHANGED: /app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MovieUi1" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.MovieUi1"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <data android:scheme="https" /> <data android:scheme="http" /> <category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity><receiver android:name=".glance.MovieAppWidgetReceiver" android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/movie_app_widget_info" /> </receiver></application> </manifest>
ADDED: /app/src/main/java/com/androidbyexample/movie/glance/MovieAppWidget.kt
package com.androidbyexample.movie.glanceimport android.content.Contextimport androidx.glance.GlanceIdimport androidx.glance.appwidget.GlanceAppWidgetimport androidx.glance.appwidget.provideContentimport androidx.glance.text.Textclass MovieAppWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { Text(text = "Widget!") } }}
ADDED: /app/src/main/java/com/androidbyexample/movie/glance/MovieAppWidgetReceiver.kt
package com.androidbyexample.movie.glanceimport androidx.glance.appwidget.GlanceAppWidgetimport androidx.glance.appwidget.GlanceAppWidgetReceiverclass MovieAppWidgetReceiver: GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = MovieAppWidget()}
ADDED: /app/src/main/res/xml/movie_app_widget_info.xml
<?xml version="1.0" encoding="utf-8"?><appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/glance_default_loading_layout" />
CHANGED: /gradle/libs.versions.toml
[versions] agp = "8.2.0-beta06" kotlin = "1.9.10" core-ktx = "1.12.0" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" lifecycle-runtime-ktx = "2.6.2" activity-compose = "1.7.2" lifecycle-compose = "2.6.2" compose-bom = "2023.09.02" appcompat = "1.6.1" material = "1.9.0" room = "2.5.2" ksp = "1.9.10-1.0.13" icons-extended = "1.6.0-alpha06"glance = "1.0.0"[libraries] core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-compose" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } ui = { group = "androidx.compose.ui", name = "ui" } ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } material3 = { group = "androidx.compose.material3", name = "material3" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } icons-extended = { group = "androidx.compose.material", name = "material-icons-extended-android", version.ref = "icons-extended"}glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" }[plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } androidLibrary = { id = "com.android.library", version.ref = "agp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }