Slide 1

Slide 1 text

FATIH GIRIS ANDROID LEAD @DNB SHOOTING A “GLANCE” AT ANDROID APP WIDGETS @fatih_grs

Slide 2

Slide 2 text

APP WIDGETS https://developer.android.com/guide/topics/appwidgets App widgets o ffi cial documentation

Slide 3

Slide 3 text

APP WIDGETS https://developer.android.com/guide/topics/appwidgets App widget processing fl ow

Slide 4

Slide 4 text

• What is Glance? • Actions • Size modes • State • Theming AGENDA

Slide 5

Slide 5 text

• Library to create App Widgets & Wear Tiles • Uses compose runtime GLANCE

Slide 6

Slide 6 text

Convert COMPOSABLES REMOTEVIEWS

Slide 7

Slide 7 text

Convert GLANCEABLES REMOTEVIEWS

Slide 8

Slide 8 text

Any Composable inside Glance is called Glanceable.

Slide 9

Slide 9 text

• Library to create App Widgets & Wear Tiles • Uses compose runtime • Converts Composables to RemoteViews & Tiles • Android 12 widget improvements • Backward compatible (API 23) GLANCE

Slide 10

Slide 10 text

SHOW ME THE CODE!

Slide 11

Slide 11 text

GLANCE class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget() }

Slide 12

Slide 12 text

GLANCE class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget() } AppWidgetReceiver

Slide 13

Slide 13 text

GLANCE class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget() } BroadcastReceiver

Slide 14

Slide 14 text

GLANCE class MyAppWidget : GlanceAppWidget() { @Composable override fun Content() { // Write your App Widget composable } }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

GLANCE

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

ACTIONS

Slide 23

Slide 23 text

• Use actions in response to user interactions • 4 types of actions - actionStartActivity - actionSendBroadcast - actionStartService - actionRunCallback ACTIONS

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

ACTIONS

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

GLANCE WIDGETS ARE STATEFUL 🤩

Slide 30

Slide 30 text

• 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

Slide 31

Slide 31 text

LET’S MAKE A CLICK COUNTER 🧮

Slide 32

Slide 32 text

STATE

Slide 33

Slide 33 text

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( “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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } }

Slide 34

Slide 34 text

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( “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( parameters = actionParametersOf(

Slide 35

Slide 35 text

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( “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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() STATE

Slide 36

Slide 36 text

// The key for the custom action parameter private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key( “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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 37

Slide 37 text

// The key for the custom action parameter private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key( “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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 38

Slide 38 text

private val COUNTER_ACTION_PARAMETER_KEY = ActionParameters.Key( “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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 39

Slide 39 text

“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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 40

Slide 40 text

“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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 41

Slide 41 text

@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( parameters = actionParametersOf( COUNTER_ACTION_PARAMETER_KEY to numberOfClicks + 1 ) ) ), text = numberOfClicks.toString() ) } } STATE

Slide 42

Slide 42 text

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) } }

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

// 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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

STATE

Slide 50

Slide 50 text

WHAT ABOUT THE SIZING?

Slide 51

Slide 51 text

• LocalSize to keep the size of a widget • 3 di ff erent size modes - SizeMode.Single - SizeMode.Exact - SizeMode.Responsive SIZING

Slide 52

Slide 52 text

SizeMode.Single • Content is called once with minimum size • LocalSize does not change

Slide 53

Slide 53 text

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}" ) } } }

Slide 54

Slide 54 text

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}" ) } } }

Slide 55

Slide 55 text

SizeMode.Single

Slide 56

Slide 56 text

SizeMode.Exact • Content is called every time size changes • LocalSize always has the latest size

Slide 57

Slide 57 text

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}" ) } } }

Slide 58

Slide 58 text

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}" ) } } }

Slide 59

Slide 59 text

SizeMode.Exact

Slide 60

Slide 60 text

NEED TO UPDATE THE WIDGET AFTER EVERY RESIZE?

Slide 61

Slide 61 text

SizeMode.Responsive • Content is called for the de fi ned sizes • LocalSize has one of the de fi ned sizes

Slide 62

Slide 62 text

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() } } }

Slide 63

Slide 63 text

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() } } }

Slide 64

Slide 64 text

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() } } }

Slide 65

Slide 65 text

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() } } }

Slide 66

Slide 66 text

SizeMode.Responsive

Slide 67

Slide 67 text

THEMING 🌈

Slide 68

Slide 68 text

• MaterialTheme THEMING

Slide 69

Slide 69 text

MaterialTheme

Slide 70

Slide 70 text

androidx.compose.material.MaterialTheme

Slide 71

Slide 71 text

• Not possible to use MaterialTheme ❌ THEMING

Slide 72

Slide 72 text

Starting from Android 12, we can use dynamic colors 🤩

Slide 73

Slide 73 text

THEMING Box( modifier = GlanceModifier .background( com.google.android.material.R.color.m3_sys_color_dynamic_dark_primary ), contentAlignment = Alignment.Center ) { Text(text = "🙈") }

Slide 74

Slide 74 text

THEMING Box( modifier = GlanceModifier .background( com.google.android.material.R.color.m3_sys_color_dynamic_dark_primary ), contentAlignment = Alignment.Center ) { Text(text = "🙈") }

Slide 75

Slide 75 text

THEMING The widget background using the dark dynamic color from the system color pallet

Slide 76

Slide 76 text

THEMING Box( modifier = GlanceModifier .background( com.google.android.material.R.color.m3_sys_color_dynamic_dark_primary ), contentAlignment = Alignment.Center ) { Text(text = "🙈") }

Slide 77

Slide 77 text

What about day & night con fi guration?

Slide 78

Slide 78 text

THEMING @color/m3_sys_color_dynamic_light_primary values/color.xml

Slide 79

Slide 79 text

THEMING @color/m3_sys_color_dynamic_light_primary values-v31/color.xml

Slide 80

Slide 80 text

THEMING @color/my_color values/color.xml

Slide 81

Slide 81 text

THEMING @color/m3_sys_color_dynamic_dark_primary values-night-v31/color.xml

Slide 82

Slide 82 text

THEMING Box( modifier = GlanceModifier .background(R.color.dynamic_color_ref), contentAlignment = Alignment.Center ) { Text(text = "🙈") }

Slide 83

Slide 83 text

THEMING Box( modifier = GlanceModifier .background(R.color.dynamic_color_ref), contentAlignment = Alignment.Center ) { Text(text = "🙈") }

Slide 84

Slide 84 text

THEMING Dynamic color reference in light mode Dynamic color reference in dark mode

Slide 85

Slide 85 text

• Not possible to use MaterialTheme ❌ • Dynamic colors THEMING

Slide 86

Slide 86 text

ColorProvider

Slide 87

Slide 87 text

ColorProvider Docs: “Provider of colors for a glance composable's attributes.”

Slide 88

Slide 88 text

ColorProvider FixedColorProvider ResourceColorProvider DayNightColorProvider

Slide 89

Slide 89 text

ColorProvider FixedColorProvider ResourceColorProvider DayNightColorProvider internal 🤔

Slide 90

Slide 90 text

ColorProvider function that provides day & night colors ColorProvider function

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

THEMING White background color in light mode Yellow background color in dark mode

Slide 94

Slide 94 text

• Not possible to use MaterialTheme ❌ • Dynamic colors • Use ColorProvider for day & night con fi guration THEMING

Slide 95

Slide 95 text

• 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

Slide 96

Slide 96 text

• 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

Slide 97

Slide 97 text

THANKS!

Slide 98

Slide 98 text

QUESTIONS @fatih_grs