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. FATIH GIRIS


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

    View Slide

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

    View Slide

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

    View Slide

  4. • What is Glance?


    • Actions


    • Size modes


    • State


    • Theming


    AGENDA

    View Slide

  5. • Library to create App Widgets & Wear Tiles


    • Uses compose runtime
    GLANCE

    View Slide

  6. Convert
    COMPOSABLES REMOTEVIEWS

    View Slide

  7. Convert
    GLANCEABLES REMOTEVIEWS

    View Slide

  8. Any Composable inside Glance
    is called Glanceable.

    View Slide

  9. • Library to create App Widgets & Wear Tiles


    • Uses compose runtime


    • Converts Composables to RemoteViews & Tiles


    • Android 12 widget improvements


    • Backward compatible (API 23)
    GLANCE

    View Slide

  10. SHOW ME THE CODE!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. GLANCE

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. ACTIONS

    View Slide

  23. • Use actions in response to user interactions


    • 4 types of actions


    - actionStartActivity
    - actionSendBroadcast
    - actionStartService
    - actionRunCallback
    ACTIONS

    View Slide

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

    View Slide

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

    View Slide

  26. ACTIONS

    View Slide

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

    View Slide

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

    View Slide

  29. GLANCE WIDGETS ARE
    STATEFUL 🤩

    View Slide

  30. • 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

    View Slide

  31. LET’S MAKE A CLICK
    COUNTER 🧮

    View Slide

  32. STATE

    View Slide

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

    View Slide

  34. 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(

    View Slide

  35. 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

    View Slide

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

    View Slide

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

    View Slide

  38. 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

    View Slide

  39. “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

    View Slide

  40. “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

    View Slide

  41. @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

    View Slide

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

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

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

    View Slide

  48. 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

    View Slide

  49. STATE

    View Slide

  50. WHAT ABOUT THE
    SIZING?

    View Slide

  51. • LocalSize to keep the size of a widget


    • 3 di
    ff
    erent size modes


    - SizeMode.Single
    - SizeMode.Exact
    - SizeMode.Responsive
    SIZING

    View Slide

  52. SizeMode.Single
    • Content is called once with minimum size


    • LocalSize does not change

    View Slide

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

    View Slide

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

    View Slide

  55. SizeMode.Single

    View Slide

  56. SizeMode.Exact
    • Content is called every time size changes


    • LocalSize always has the latest size

    View Slide

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

    View Slide

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

    View Slide

  59. SizeMode.Exact

    View Slide

  60. NEED TO UPDATE THE
    WIDGET AFTER EVERY
    RESIZE?

    View Slide

  61. SizeMode.Responsive
    • Content is called for the de
    fi
    ned sizes


    • LocalSize has one of the de
    fi
    ned sizes

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  66. SizeMode.Responsive

    View Slide

  67. THEMING 🌈

    View Slide

  68. • MaterialTheme
    THEMING

    View Slide

  69. MaterialTheme

    View Slide

  70. androidx.compose.material.MaterialTheme

    View Slide

  71. • Not possible to use MaterialTheme ❌
    THEMING

    View Slide

  72. Starting from Android 12, we can use
    dynamic colors 🤩

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  77. What about day & night


    con
    fi
    guration?

    View Slide

  78. THEMING

    @color/m3_sys_color_dynamic_light_primary

    values/color.xml

    View Slide

  79. THEMING

    @color/m3_sys_color_dynamic_light_primary

    values-v31/color.xml

    View Slide

  80. THEMING

    @color/my_color

    values/color.xml

    View Slide

  81. THEMING

    @color/m3_sys_color_dynamic_dark_primary

    values-night-v31/color.xml

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  85. • Not possible to use MaterialTheme ❌
    • Dynamic colors
    THEMING

    View Slide

  86. ColorProvider

    View Slide

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

    View Slide

  88. ColorProvider
    FixedColorProvider ResourceColorProvider DayNightColorProvider

    View Slide

  89. ColorProvider
    FixedColorProvider ResourceColorProvider DayNightColorProvider
    internal 🤔

    View Slide

  90. ColorProvider function that provides day & night colors
    ColorProvider function

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  95. • 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

    View Slide

  96. • 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

    View Slide

  97. THANKS!

    View Slide

  98. QUESTIONS
    @fatih_grs

    View Slide