Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Shooting a “Glance” at Android App Widgets

Shooting a “Glance” at Android App Widgets

The Android community adopts Jetpack Compose more and more every day. But what about our app widgets?

In this talk, we will see how we can use “Glance” library to create app widgets. We will briefly talk about how to create “glanceable” UI using composables. We will also see how to use actions for user interactions and create widgets with different size modes. Moreover, we will touch upon the internal state of the Glance app widget and how to update this state outside of the widget process. Finally, we will discuss how to apply theming and use dynamic colors in the app widgets.

Fatih Giriş

June 25, 2022
Tweet

More Decks by Fatih Giriş

Other Decks in Technology

Transcript

  1. • Library to create App Widgets & Wear Tiles •

    Uses compose runtime • Converts Composables to RemoteViews & Tiles • Android 12 widget improvements • Backward compatible (API 23) GLANCE
  2. GLANCE class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { androidx.compose.material.Text( text = "Hello from a Glance widget!" ) } }
  3. GLANCE class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { androidx.glance.text.Text( text = "Hello from a Glance widget!" ) } }
  4. GLANCE class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { androidx.glance.text.Text( modifier = androidx.compose.ui.Modifier, text = "Hello from a Glance widget!" ) } }
  5. GLANCE class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { androidx.glance.text.Text( modifier = androidx.glance.GlanceModifier, text = "Hello from a Glance widget!" ) } }
  6. • Use actions in response to user interactions • 4

    types of actions - actionStartActivity - actionSendBroadcast - actionStartService - actionRunCallback ACTIONS
  7. ACTIONS class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { Text( modifier = GlanceModifier.clickable( actionStartActivity<MyActivity>() ), text = "Hello from a Glance widget!" ) } }
  8. ACTIONS class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { Text( modifier = GlanceModifier.clickable( actionStartActivity<MyActivity>() ), text = "Hello from a Glance widget!" ) } }
  9. ACTIONS class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { Text( modifier = GlanceModifier.clickable( actionStartActivity<MyActivity>() ), text = "Hello from a Glance widget!" ) } }
  10. ACTIONS class MyAppWidget : GlanceAppWidget() { @Composable override fun Content()

    { Text( modifier = GlanceModifier.clickable( actionRunCallback<MyCustomActionCallback>() ), text = "Hello from a Glance widget!" ) } }
  11. • Provides a composition local LocalState • Uses DataStore under

    the hood • Default state de fi nition supports: Boolean, Int, Long, Float, String, Set • Get the current state using currentState() STATE
  12. STATE class MyAppWidget : GlanceAppWidget() { // The key for

    the data store private val COUNTER_KEY = intPreferencesKey("MY_COUNTER_KEY") // The key for the custom action parameter private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } }
  13. STATE class MyAppWidget : GlanceAppWidget() { // The key for

    the data store private val COUNTER_KEY = intPreferencesKey("MY_COUNTER_KEY") // The key for the custom action parameter private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf(
  14. class MyAppWidget : GlanceAppWidget() { // The key for the

    data store private val COUNTER_KEY = intPreferencesKey("MY_COUNTER_KEY") // The key for the custom action parameter private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() STATE
  15. // The key for the custom action parameter private val

    COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  16. // The key for the custom action parameter private val

    COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  17. private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key<Int>( “COUNTER_INCREMENT_ACTION” ) @Composable override fun

    Content() { // Get the saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  18. “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the

    saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  19. “COUNTER_INCREMENT_ACTION” ) @Composable override fun Content() { // Get the

    saved number of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  20. @Composable override fun Content() { // Get the saved number

    of clicks from the current state val numberOfClicks = currentState(key = COUNTER_KEY) ?: 0 Text( modifier = GlanceModifier.clickable( actionRunCallback<CounterIncrementActionCallback>( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE
  21. STATE class CounterIncrementActionCallback : ActionCallback { override suspend fun onRun(

    context: Context, glanceId: GlanceId, parameters: ActionParameters ) { // Get the incremented value from the parameters since // we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter } } // Need to update the widget as well in order // to invoke the content function again MyAppWidget().update(context, glanceId) } }
  22. STATE class CounterIncrementActionCallback : ActionCallback { override suspend fun onRun(

    context: Context, glanceId: GlanceId, parameters: ActionParameters ) { // Get the incremented value from the parameters since // we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter
  23. class CounterIncrementActionCallback : ActionCallback { override suspend fun onRun( context:

    Context, glanceId: GlanceId, parameters: ActionParameters ) { // Get the incremented value from the parameters since // we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter STATE
  24. class CounterIncrementActionCallback : ActionCallback { override suspend fun onRun( context:

    Context, glanceId: GlanceId, parameters: ActionParameters ) { // Get the incremented value from the parameters since // we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter } } // Need to update the widget as well in order // to invoke the content function again STATE
  25. context: Context, glanceId: GlanceId, parameters: ActionParameters ) { // Get

    the incremented value from the parameters since // we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter } } // Need to update the widget as well in order // to invoke the content function again MyAppWidget().update(context, glanceId) } } STATE
  26. // Get the incremented value from the parameters since //

    we are sending it from the composable val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter } } // Need to update the widget as well in order // to invoke the content function again MyAppWidget().update(context, glanceId) } } STATE
  27. val counter = parameters[MyAppWidget.COUNTER_ACTION_PARAMETER_KEY] ?: 0 // Save the counter

    into the data store updateAppWidgetState( context = context, definition = PreferencesGlanceStateDefinition, glanceId = glanceId ) { it.toMutablePreferences().apply { this[MyAppWidget.COUNTER_KEY] = counter } } // Need to update the widget as well in order // to invoke the content function again MyAppWidget().update(context, glanceId) } } STATE
  28. • LocalSize to keep the size of a widget •

    3 di ff erent size modes - SizeMode.Single - SizeMode.Exact - SizeMode.Responsive SIZING
  29. SizeMode.Single class MyAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode

    get() = SizeMode.Single @Composable override fun Content() { Box { Text( text = "I am a using SizeMode.Single" + "Content will be called only once" + "${LocalSize.current}" ) } } }
  30. SizeMode.Single class MyAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode

    get() = SizeMode.Single @Composable override fun Content() { Box { Text( text = "I am a using SizeMode.Single" + "Content will be called only once" + "${LocalSize.current}" ) } } }
  31. SizeMode.Exact class MyAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode

    get() = SizeMode.Exact @Composable override fun Content() { Box { Text( text = "I am a using SizeMode.Single" + "Content will be called only once" + "${LocalSize.current}" ) } } }
  32. SizeMode.Exact class MyAppWidget : GlanceAppWidget() { override val sizeMode: SizeMode

    get() = SizeMode.Exact @Composable override fun Content() { Box { Text( text = "I am a using SizeMode.Single" + "Content will be called only once" + "${LocalSize.current}" ) } } }
  33. SizeMode.Responsive • Content is called for the de fi ned

    sizes • LocalSize has one of the de fi ned sizes
  34. SizeMode.Responsive class MyAppWidget : GlanceAppWidget() { companion object { //

    Responsive sizes val SMALL_BOX = DpSize(100.dp, 100.dp) val LARGE_BOX = DpSize(200.dp, 200.dp) } override val sizeMode: SizeMode get() = SizeMode.Responsive( setOf(SMALL_BOX, LARGE_BOX) ) @Composable override fun Content() { when (LocalSize.current) { SMALL_BOX -> SmallBox() LARGE_BOX -> LargeBox() } } }
  35. SizeMode.Responsive class MyAppWidget : GlanceAppWidget() { companion object { //

    Responsive sizes val SMALL_BOX = DpSize(100.dp, 100.dp) val LARGE_BOX = DpSize(200.dp, 200.dp) } override val sizeMode: SizeMode get() = SizeMode.Responsive( setOf(SMALL_BOX, LARGE_BOX) ) @Composable override fun Content() { when (LocalSize.current) { SMALL_BOX -> SmallBox() LARGE_BOX -> LargeBox() } } }
  36. SizeMode.Responsive class MyAppWidget : GlanceAppWidget() { companion object { //

    Responsive sizes val SMALL_BOX = DpSize(100.dp, 100.dp) val LARGE_BOX = DpSize(200.dp, 200.dp) } override val sizeMode: SizeMode get() = SizeMode.Responsive( setOf(SMALL_BOX, LARGE_BOX) ) @Composable override fun Content() { when (LocalSize.current) { SMALL_BOX -> SmallBox() LARGE_BOX -> LargeBox() } } }
  37. SizeMode.Responsive class MyAppWidget : GlanceAppWidget() { companion object { //

    Responsive sizes val SMALL_BOX = DpSize(100.dp, 100.dp) val LARGE_BOX = DpSize(200.dp, 200.dp) } override val sizeMode: SizeMode get() = SizeMode.Responsive( setOf(SMALL_BOX, LARGE_BOX) ) @Composable override fun Content() { when (LocalSize.current) { SMALL_BOX -> SmallBox() LARGE_BOX -> LargeBox() } } }
  38. THEMING Box( modifier = GlanceModifier .background( ColorProvider( day = Color.White,

    night = Color.Yellow ) ), contentAlignment = Alignment.Center ) { Text(text = "🙈") }
  39. THEMING Box( modifier = GlanceModifier .background( ColorProvider( day = Color.White,

    night = Color.Yellow ) ), contentAlignment = Alignment.Center ) { Text(text = "🙈") }
  40. • Not possible to use MaterialTheme ❌ • Dynamic colors

    • Use ColorProvider for day & night con fi guration THEMING
  41. • Glance converts Composables to RemoteViews (and Tiles) • Not

    possible to use the composables from the app • Actions: • actionStartActivity • actionStartService • actionSendBroadcast • actionRunCallback • Size modes: Single, Exact and Responsive TL;DR
  42. • currentState() provides the current state of the widget •

    Dynamic colors on Android 12 and above • ColorProvider()supports day & night colors • Remember that it’s still in alpha TL;DR