Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

16. Creating Widgets Using Jetpack Glance
Written by Denis Buketa

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

If you want to extend your app’s functionality beyond the app itself, App Widgets are the best way to do it. They allow you to provide some features of your app as at-a-glance views that live in your user’s home screen.

According to Google’s blog announcement for Jetpack Glance, 84% of users use at least one widget. That gives you a sense of how important it is to at least be familiar with the basics of building app widgets.

In this chapter, you’ll learn how simple it is to build app widgets using Jetpack Glance.

You’ll learn:

  • What is Jetpack Glance.
  • How to define essential characteristics of your app widget using AppWidgetProvider.
  • How to use GlanceAppWidgetReceiver to instantiate your app widget and update it.
  • How to create UI layouts using GlanceAppWidget.
  • How to handle actions in the widget.

Note: If you want to check Google’s announcement of Jetpack Glance, please refer to the official Google blog: https://android-developers.googleblog.com/2021/12/announcing-jetpack-glance-alpha-for-app.html.

Introducing Jetpack Glance

Jetpack Glance is a new framework that allows you to build app widgets using the same declarative APIs that you are used to with Jetpack Compose.

Beside using the similar APIs, it uses Jetpack Compose Runtime to translate a Composable into a RemoteView, which it then displays in an app widget. It also depends on Jetpack Compose Graphics and UI layers that you covered in the very first chapter of this book.

Glance Glance-appwidget XML RemoteViews GlanceAppWidgetReceiver Compose Runtime ...
Glance Structure

Keep in mind that these dependencies mean that Glance requires for you to enable Compose in your project, but it’s not directly interoperable with other Jetpack Compose UI elements. Because of that, you’ll notice that in this chapter you’ll use GlanceModifier instead of Modifier and some other composables will be imported from androidx.glance package instead of androidx.compose package.

Defining Essential Characteristics of Your App Widget

To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.

Subreddits Screen
Fudpojyuxz Qhfion

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/app_widget_description"
  android:minWidth="100dp"
  android:minHeight="100dp"
  android:minResizeHeight="100dp"
  android:minResizeWidth="100dp"
  android:initialLayout="@layout/app_widget_loading"
  android:previewLayout="@layout/app_widget_subreddits_preview"
  android:resizeMode="horizontal|vertical"
  android:targetCellWidth="3"
  android:targetCellHeight="3"
  android:widgetCategory="home_screen">
</appwidget-provider>

Creating the Hello Glance Widget

In this section you’ll create a Hello Glance widget so that you get one step closer to displaying your widget on screen.

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.text.Text

class SubredditsWidget : GlanceAppWidget() {
  @Composable
  override fun Content() {
    Box(
      modifier = GlanceModifier
        .fillMaxSize()
        .background(Color.White),
      contentAlignment = Alignment.Center
    ) {
      Text(text = "Hello Glance")
    }
  }
}

Displaying the Widget on the Home Screen

The final step before you can see your widget on the home screen is to create AppWidgetProvider.

class SubredditsWidgetReceiver : GlanceAppWidgetReceiver() {

  override val glanceAppWidget: GlanceAppWidget =
    SubredditsWidget()
}
import androidx.glance.appwidget.GlanceAppWidgetReceiver
<receiver
  android:name=".appwidget.SubredditsWidgetReceiver"
  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/app_widget_subreddits" />
</receiver>
Widget Option in Launcher
Duxluz Anzeow ej Yiizlpic

Widget Preview
Folroz Gwiraaj

Widget on Home Screen
Yupyep oq Cojo Xrsiip

Adding Subreddits to the Widget

Before you start implementing the rest of the UI, it is important to mention that @Preview still doesn’t work for Jetpack Glance. You’ll be coding the UI for the widget and then building the project later to see it.

@Composable
fun Subreddit(@StringRes id: Int) {
  val checked: Boolean = false

  Row(
    modifier = GlanceModifier
      .padding(top = 16.dp)
      .fillMaxWidth(),
    verticalAlignment = Alignment.CenterVertically
  ) {

    Image(
      provider = ImageProvider(
        R.drawable.subreddit_placeholder
      ),
      contentDescription = null,
      modifier = GlanceModifier.size(24.dp)
    )

    Text(
      text = LocalContext.current.getString(id),
      modifier = GlanceModifier
        .padding(start = 16.dp)
        .defaultWeight(),
      style = TextStyle(
        color = FixedColorProvider(
          color = MaterialTheme.colors.primaryVariant
        ),
        fontSize = 10.sp,
        fontWeight = FontWeight.Bold
      )
    )

    Switch(
      checked = checked,
      onCheckedChange = null
    )
  }
}
import androidx.annotation.StringRes
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.LocalContext
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.appwidget.Switch
import androidx.glance.layout.Row
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.layout.fillMaxWidth
import androidx.glance.text.FontWeight
import androidx.glance.text.TextStyle
import androidx.glance.unit.FixedColorProvider
import com.yourcompany.android.jetreddit.R
@Composable
fun WidgetTitle() {
  Text(
    text = "Subreddits",
    modifier = GlanceModifier.fillMaxWidth(),
    style = TextStyle(
      fontWeight = FontWeight.Bold,
      fontSize = 18.sp,
      color = FixedColorProvider(Color.Black)
    ),
  )
}

@Composable
fun ScrollableSubredditsList() {
  LazyColumn {
    items(communities) { communityId ->
      Subreddit(id = communityId)
    }
  }
}
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items
import com.yourcompany.android.jetreddit.screens.communities
@Composable
override fun Content() {
  Column(
    modifier = GlanceModifier
      .fillMaxSize()
      .padding(16.dp)
      .appWidgetBackground()
      .background(Color.White)
      .cornerRadius(16.dp)
  ) {
    WidgetTitle()
    ScrollableSubredditsList()
  }
}
import androidx.glance.layout.Column
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.cornerRadius
Subreddits Widget
Gifcuyhugk Vamtol

Handling Widget Actions

First, let’s handle the onCheckedChange() action in Switch(). When working with widgets, you handle user actions using the ActionCallback interface.

private val toggledSubredditIdKey = ActionParameters.Key<String>("ToggledSubredditIdKey")

class SwitchToggleAction : ActionCallback {
  override suspend fun onAction(
    context: Context,
    glanceId: GlanceId,
    parameters: ActionParameters
  ) {
    val toggledSubredditId: String =
      requireNotNull(parameters[toggledSubredditIdKey])
    val checked: Boolean =
      requireNotNull(parameters[ToggleableStateKey])

    updateAppWidgetState(context, glanceId) { glancePrefs ->
      glancePrefs[booleanPreferencesKey(toggledSubredditId)] = checked
    }

    SubredditsWidget().update(context, glanceId)
  }
}
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.ToggleableStateKey
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.GlanceId
import androidx.datastore.preferences.core.booleanPreferencesKey
import android.content.Context
@Composable
fun Subreddit(@StringRes id: Int) {

  // HERE
  val preferences: Preferences = currentState()
  val checked: Boolean = preferences[booleanPreferencesKey(id.toString())] ?: false

  Row(...) {

    ...

    Switch(
      checked = checked,
      // HERE
      onCheckedChange = actionRunCallback<SwitchToggleAction>(
        actionParametersOf(
          toggledSubredditIdKey to id.toString()
        )
      )
    )
  }
}
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.action.actionParametersOf
import androidx.glance.currentState
import androidx.datastore.preferences.core.Preferences

Connecting the Widget With the JetReddit App

At the beginning of the chapter, it was mentioned that the JetReddit app now stores states of the switches in preferences.

override suspend fun onAction(
  context: Context,
  glanceId: GlanceId,
  parameters: ActionParameters
) {
  val toggledSubredditId: String = requireNotNull(parameters[toggledSubredditIdKey])
  val checked: Boolean = requireNotNull(parameters[ToggleableStateKey])

  updateAppWidgetState(context, glanceId) { glancePreferences ->
    glancePreferences[booleanPreferencesKey(toggledSubredditId)] = checked
  }

  // HERE
  context.dataStore.edit { appPreferences ->
    appPreferences[booleanPreferencesKey(toggledSubredditId)] = checked
  }

  SubredditsWidget().update(context, glanceId)
}
import com.yourcompany.android.jetreddit.dependencyinjection.dataStore
import androidx.datastore.preferences.core.edit
private suspend fun updateAppWidgetPreferences(
  subredditIdToCheckedMap: Map<Int, Boolean>,
  context: Context,
  glanceId: GlanceId
) {
  subredditIdToCheckedMap.forEach { (subredditId, checked) ->
    updateAppWidgetState(context, glanceId) { state ->
      state[booleanPreferencesKey(subredditId.toString())] = checked
    }
  }
}

private fun Preferences.toSubredditIdToCheckedMap(): Map<Int, Boolean> {
  return communities.associateWith { communityId ->
    this[booleanPreferencesKey(communityId.toString())] ?: false
  }
}
private val coroutineScope = MainScope()

override fun onUpdate(
  context: Context,
  appWidgetManager: AppWidgetManager,
  appWidgetIds: IntArray
) {
  super.onUpdate(context, appWidgetManager, appWidgetIds)

  coroutineScope.launch {

    // Step 1: Get GlanceId for your widget
    val glanceId: GlanceId? = GlanceAppWidgetManager(context)
      .getGlanceIds(SubredditsWidget::class.java)
      .firstOrNull()

    if (glanceId != null) {
      // Step 2: Collect JetReddit's preferences
      withContext(Dispatchers.IO) {
        context.dataStore.data
          .map { preferences -> preferences.toSubredditIdToCheckedMap() }
          .collect { subredditIdToCheckedMap ->

            // Step 3: Update app widget state
            updateAppWidgetPreferences(subredditIdToCheckedMap, context, glanceId)

            // Step 4: Update app widget content
            glanceAppWidget.update(context, glanceId)
          }
      }
    }
  }
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
  super.onDeleted(context, appWidgetIds)
  coroutineScope.cancel()
}
import android.appwidget.AppWidgetManager
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.map
import androidx.glance.appwidget.GlanceAppWidgetManager
Widget connected with JetReddit
Quwsoc hercafjuz zuds FixXivciz

Key Points

  • Jetpack Glance is a new framework that allows you to build app widgets using declarative APIs that you are used to with Jetpack Compose.
  • To define widget’s essential characteristic, you need to create a appwidget-provider in res/xml folder.
  • You use GlanceAppWidget to define the widget UI and to communicate with the AppWidgetManager.
  • You need to create GlanceAppWidgetReceiver and define in the AndroidManifest.xml for your app to be able to create widgets.
  • If you want to handle widget actions, you’ll use ActionCallback.
  • ActionParameters enables you to pass data between app widget and ActionCallback.
  • To make your app widget stateful, you can update its state with updateAppWidgetState().
  • The onUpdate() method in GlanceAppWidgetReceiver() allows you to update the widget state once the user creates it.

Where to Go From here?

Congratulations, you just completed the Creating Widgets Using Jetpack Glance chapter!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now