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

Take a look at Jetpack Glance

Piotr Prus
November 07, 2022

Take a look at Jetpack Glance

Jetpack Glance is the new shiny library in the Jetpack family for creating widgets. At first glance, it resembles a Jetpack Compose, but in many cases, it is really different. Learn those differences and create beautiful widgets easily.
During this talk, I will describe how the Jetpack Glance works, what are the differences between it and RemoteViews widgets. You will also learn why we still need some XMLs, how to manage the state, and periodic updates.

Piotr Prus

November 07, 2022
Tweet

More Decks by Piotr Prus

Other Decks in Programming

Transcript

  1. Agenda: • Introduction to widgets • Widget providers • Composing

    widget UI • State management • Trigger widget update • Change state outside widget class • Initial con fi guration • Open from widget • Why we still need XML • Thoughts and conclusion
  2. androidx.glance:glance-appwidget:1.0.0-alpha03 android { buildFeatures { compose = true } composeOptions

    { kotlinCompilerExtensionVersion = "1.1.0-beta03" } kotlinOptions { jvmTarget = "1.8" } }
  3. class TestWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context?, intent:

    Intent?) { super.onReceive(context, intent) } override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) } }
  4. class TestWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context?, intent:

    Intent?) { super.onReceive(context, intent) } override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) } }
  5. class TestWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context?, intent:

    Intent?) { super.onReceive(context, intent) } override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) } }
  6. class TestWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context?, intent:

    Intent?) { super.onReceive(context, intent) } override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) } }
  7. class TestWidgetProvider : AppWidgetProvider() { override fun onReceive(context: Context?, intent:

    Intent?) { super.onReceive(context, intent) } override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) } } class WeatherWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget get() = WeatherWidget() }
  8. data class WidgetState( val data: WeatherItem, val loading: Boolean, )

    data class WeatherItem( val address: String? = null, val latitude: Double, val longitude: Double, val temperature: Int, val windSpeed: Int, val humidity: Int, val pressure: Int )
  9. class WeatherWidget(private val state: WidgetState) : GlanceAppWidget() { @Composable override

    fun Content() { Box( modifier = GlanceModifier.fillMaxSize() .background(ImageProvider(resId = R.drawable.shape_widget_small)) .appWidgetBackground(), contentAlignment = Alignment.Center ) { WidgetBody(state = state) } } }
  10. class WeatherWidget(private val state: WidgetState) : GlanceAppWidget() { @Composable override

    fun Content() { Box( modifier = GlanceModifier.fillMaxSize() .background(ImageProvider(resId = R.drawable.shape_widget_small)) .appWidgetBackground(), contentAlignment = Alignment.Center ) { WidgetBody(state = state) } } }
  11. class WeatherWidget(private val state: WidgetState) : GlanceAppWidget() { @Composable override

    fun Content() { Box( modifier = GlanceModifier.fillMaxSize() .cornerRadius(8.dp) .appWidgetBackground(), contentAlignment = Alignment.Center ) { WidgetBody(state = state) } } }
  12. class WeatherWidget(private val state: WidgetState) : GlanceAppWidget() { @Composable override

    fun Content() { Box( modifier = GlanceModifier.fillMaxSize() .cornerRadius(8.dp) .appWidgetBackground(), contentAlignment = Alignment.Center ) { WidgetBody(state = state) } } } Min API 31
  13. @Composable fun WidgetBody(state: WidgetState) { Column() { Row() { Image()

    Spacer() Text() Spacer() Image() } Spacer() Row() { Image() Spacer() Text() Spacer() Image() Spacer() Text() } } } import androidx.glance.Image import androidx.glance.layout.Column import androidx.glance.layout.Row import androidx.glance.layout.Spacer import androidx.glance.text.Text
  14. Glance components: • Column • LazyColumn • Row • Box

    • Image • CircularProgressIndicator • Text • Button • Spacer Glance modi fi er: • Height • Width • Size • Padding • Background • Corner radius • Visibility • Clickable
  15. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  16. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  17. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  18. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  19. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  20. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  21. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  22. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  23. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  24. Image( modifier = GlanceModifier.size(12.dp), provider = ImageProvider(resId = R.drawable.outline_location_on_24), contentDescription

    = "Location icon" ) Spacer(modifier = GlanceModifier.width(4.dp)) Text( modifier = GlanceModifier.fillMaxWidth().defaultWeight(), text = state.data.address ?: "null", style = TextStyle( fontWeight = FontWeight.Normal, fontSize = 10.sp, textAlign = TextAlign.Start, color = ColorProvider(day = Color.Black, night = Color.White) ), maxLines = 1 )
  25. Widget GlanceStateDe fi nition Preferences by default GlanceId internal data

    class AppWidgetId(val appWidgetId: Int) : GlanceId
  26. SAVE Preferences prefs[doublePreferencesKey(widgetLatitudeKey)] = 54.40 prefs[doublePreferencesKey(widgetLongitudeKey)] = 18.3 GET val

    latitude = prefs[doublePreferencesKey(widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(widgetLongitudeKey)] ?: Double.MIN_VALUE
  27. object WidgetStateHelper { fun save(prefs: MutablePreferences, state: WeatherItem) {} fun

    saveLocation(prefs: MutablePreferences, latitude: Double, longitude: Double) {} fun isStored(prefs: MutablePreferences, latitude: Double, longitude: Double): Boolean = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] == latitude && prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] == longitude fun setLoading(prefs: MutablePreferences, loading: Boolean) {} }
  28. object WidgetStateHelper { fun getState(prefs: Preferences): WidgetState { val address

    = prefs[stringPreferencesKey(WidgetStateHelper.widgetAddressKey)] ?: "" val latitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] ?: Double.MIN_VALUE val temperature = prefs[intPreferencesKey(WidgetStateHelper.widgetTemperatureKey)] ?: Int.MIN_VALUE val humidity = prefs[intPreferencesKey(WidgetStateHelper.widgetHumidityKey)] ?: Int.MIN_VALUE val wind = prefs[intPreferencesKey(WidgetStateHelper.widgetWindKey)] ?: Int.MIN_VALUE val loading = prefs[booleanPreferencesKey(WidgetStateHelper.widgetLoadingKey)] ?: false return WidgetState( data = WeatherItem( address = address, latitude = latitude, longitude = longitude, temperature = temperature, windSpeed = wind, humidity = humidity, pressure = 0 ), loading = loading ) }
  29. object WidgetStateHelper { fun getState(prefs: Preferences): WidgetState { val address

    = prefs[stringPreferencesKey(WidgetStateHelper.widgetAddressKey)] ?: "" val latitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] ?: Double.MIN_VALUE val temperature = prefs[intPreferencesKey(WidgetStateHelper.widgetTemperatureKey)] ?: Int.MIN_VALUE val humidity = prefs[intPreferencesKey(WidgetStateHelper.widgetHumidityKey)] ?: Int.MIN_VALUE val wind = prefs[intPreferencesKey(WidgetStateHelper.widgetWindKey)] ?: Int.MIN_VALUE val loading = prefs[booleanPreferencesKey(WidgetStateHelper.widgetLoadingKey)] ?: false return WidgetState( data = WeatherItem( address = address, latitude = latitude, longitude = longitude, temperature = temperature, windSpeed = wind, humidity = humidity, pressure = 0 ), loading = loading ) }
  30. object WidgetStateHelper { fun getState(prefs: Preferences): WidgetState { val address

    = prefs[stringPreferencesKey(WidgetStateHelper.widgetAddressKey)] ?: "" val latitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] ?: Double.MIN_VALUE val temperature = prefs[intPreferencesKey(WidgetStateHelper.widgetTemperatureKey)] ?: Int.MIN_VALUE val humidity = prefs[intPreferencesKey(WidgetStateHelper.widgetHumidityKey)] ?: Int.MIN_VALUE val wind = prefs[intPreferencesKey(WidgetStateHelper.widgetWindKey)] ?: Int.MIN_VALUE val loading = prefs[booleanPreferencesKey(WidgetStateHelper.widgetLoadingKey)] ?: false return WidgetState( data = WeatherItem( address = address, latitude = latitude, longitude = longitude, temperature = temperature, windSpeed = wind, humidity = humidity, pressure = 0 ), loading = loading ) }
  31. object WidgetStateHelper { fun getState(prefs: Preferences): WidgetState { val address

    = prefs[stringPreferencesKey(WidgetStateHelper.widgetAddressKey)] ?: "" val latitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] ?: Double.MIN_VALUE val temperature = prefs[intPreferencesKey(WidgetStateHelper.widgetTemperatureKey)] ?: Int.MIN_VALUE val humidity = prefs[intPreferencesKey(WidgetStateHelper.widgetHumidityKey)] ?: Int.MIN_VALUE val wind = prefs[intPreferencesKey(WidgetStateHelper.widgetWindKey)] ?: Int.MIN_VALUE val loading = prefs[booleanPreferencesKey(WidgetStateHelper.widgetLoadingKey)] ?: false return WidgetState( data = WeatherItem( address = address, latitude = latitude, longitude = longitude, temperature = temperature, windSpeed = wind, humidity = humidity, pressure = 0 ), loading = loading ) }
  32. object WidgetStateHelper { fun getState(prefs: Preferences): WidgetState { val address

    = prefs[stringPreferencesKey(WidgetStateHelper.widgetAddressKey)] ?: "" val latitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLatitudeKey)] ?: Double.MIN_VALUE val longitude = prefs[doublePreferencesKey(WidgetStateHelper.widgetLongitudeKey)] ?: Double.MIN_VALUE val temperature = prefs[intPreferencesKey(WidgetStateHelper.widgetTemperatureKey)] ?: Int.MIN_VALUE val humidity = prefs[intPreferencesKey(WidgetStateHelper.widgetHumidityKey)] ?: Int.MIN_VALUE val wind = prefs[intPreferencesKey(WidgetStateHelper.widgetWindKey)] ?: Int.MIN_VALUE val loading = prefs[booleanPreferencesKey(WidgetStateHelper.widgetLoadingKey)] ?: false return WidgetState( data = WeatherItem( address = address, latitude = latitude, longitude = longitude, temperature = temperature, windSpeed = wind, humidity = humidity, pressure = 0 ), loading = loading ) }
  33. class WeatherWidget() : GlanceAppWidget() { @Composable override fun Content() {

    val state = WidgetStateHelper.getState(currentState()) } }
  34. class WeatherWidget() : GlanceAppWidget() { @Composable override fun Content() {

    val state = WidgetStateHelper.getState(currentState()) } } Retrieves the current customisable store for view speci fi c state data as de fi ned by GlanceStateDe fi nition in the surface implementation.
  35. Update state Trigger composition public suspend fun updateAppWidgetState( context: Context,

    glanceId: GlanceId, updateState: suspend (MutablePreferences) -> Unit, ) { updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { it.toMutablePreferences().apply { updateState(this) } } } public suspend fun update(context: Context, glanceId: GlanceId) { require(glanceId is AppWidgetId) { "The glanceId '$glanceId' is not a valid App Widget glance id" } update(context, AppWidgetManager.getInstance(context), glanceId.appWidgetId) }
  36. Where should we make computation for the widget ? Widget

    Service ViewModel BroadcastReceiver WorkManager
  37. Where should we make computation for the widget ? Widget

    Service ViewModel BroadcastReceiver WorkManager
  38. Where should we make computation for the widget ? Widget

    Service ViewModel BroadcastReceiver WorkManager
  39. How to trigger the update? Image( modifier = GlanceModifier.size(32.dp).padding(6.dp) .clickable(onClick

    = actionRunCallback<WidgetRefreshAction>()), provider = ImageProvider(resId = R.drawable.ic_refresh), contentDescription = "Refresh icon" )
  40. How to trigger the update? Image( modifier = GlanceModifier.size(32.dp).padding(6.dp) .clickable(onClick

    = actionRunCallback<WidgetRefreshAction>()), provider = ImageProvider(resId = R.drawable.ic_refresh), contentDescription = "Refresh icon" )
  41. How to trigger the update? Image( modifier = GlanceModifier.size(32.dp).padding(6.dp) .clickable(onClick

    = actionRunCallback<WidgetRefreshAction>()), provider = ImageProvider(resId = R.drawable.ic_refresh), contentDescription = "Refresh icon" ) class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { } }
  42. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  43. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  44. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  45. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  46. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  47. class WidgetRefreshAction : ActionCallback { override suspend fun onRun(context: Context,

    glanceId: GlanceId, parameters: ActionParameters) { val prefs = WeatherWidget().getAppWidgetState<Preferences>(context, glanceId) val location = WidgetStateHelper.getState(prefs).let { it.data.latitude to it.data.longitude } val oneTimeWorkRequest = OneTimeWorkRequestBuilder<WeatherWidgetWorker>() .setInputData( WeatherWidgetWorker.buildData( latitude = location.first, longitude = location.second ) ) .build() WorkManager.getInstance(context) .enqueue(oneTimeWorkRequest) } }
  48. WeatherWidget().updateAppWidgetState(appContext, glanceId) { prefs -> // Update state } //

    Trigger composition of Content() WeatherWidget().update(appContext, glanceId) private suspend fun updateWidgetState( glanceId: GlanceId, update: (MutablePreferences) -> Unit ) { WeatherWidget().apply { updateAppWidgetState(appContext, glanceId) { update(it) } update(appContext, glanceId) } }
  49. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) {
  50. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  51. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  52. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  53. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  54. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  55. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  56. class WeatherWidgetWorker( private val repository: WeatherRepository, private val appContext: Context,

    private val workerParameters: WorkerParameters ) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { return getLocation()?.let { locationPair -> val glanceId = GlanceAppWidgetManager(appContext) .getGlanceIds(WeatherWidget::class.java).firstOrNull { id -> WeatherWidget().getAppWidgetState<Preferences>( appContext, id ).let { prefs -> WidgetStateHelper.isStored( prefs, latitude = locationPair.first, longitude = locationPair.second ) } } ?: return Result.failure() updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, true) }
  57. override suspend fun doWork(): Result { … updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it,

    true) } repository.getData(latitude = locationPair.first, longitude = locationPair.second) .onSuccess { item -> updateWidgetState(glanceId) { WidgetStateHelper.save(it, item) } return Result.success() } .onFailure { throwable -> updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, false) } return Result.retry() }
  58. override suspend fun doWork(): Result { … updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it,

    true) } repository.getData(latitude = locationPair.first, longitude = locationPair.second) .onSuccess { item -> updateWidgetState(glanceId) { WidgetStateHelper.save(it, item) } return Result.success() } .onFailure { throwable -> updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, false) } return Result.retry() }
  59. override suspend fun doWork(): Result { … updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it,

    true) } repository.getData(latitude = locationPair.first, longitude = locationPair.second) .onSuccess { item -> updateWidgetState(glanceId) { WidgetStateHelper.save(it, item) } return Result.success() } .onFailure { throwable -> updateWidgetState(glanceId) { WidgetStateHelper.setLoading(it, false) } return Result.retry() }
  60. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  61. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  62. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  63. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  64. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  65. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  66. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  67. lifecycleScope.launch { val glanceId = GlanceAppWidgetManager(context).getGlanceIds( WeatherWidget::class.java ).last() WeatherWidget().apply {

    updateAppWidgetState(context, glanceId) { WidgetStateHelper.saveLocation(it, latitude = item.latitude, longitude = item.longitude) WidgetStateHelper.saveAddress(it, address = item.name) } update(context, glanceId) } startWeatherWorker(latitude = item.latitude, longitude = item.longitude) setResult(RESULT_OK, intent) finish() } On item click
  68. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  69. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  70. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  71. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  72. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  73. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  74. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  75. fun Context.startWeatherWorker(latitude: Double, longitude: Double) { val networkConstraint = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

    val request = PeriodicWorkRequest .Builder(WeatherWidgetWorker::class.java, 15, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5000L, TimeUnit.MILLISECONDS) .setInputData( WeatherWidgetWorker.buildData(latitude, longitude) ) .setConstraints(networkConstraint) .build() val uniqueTag = WeatherWidget.UNIQUE_WORK_TAG + "_$latitude" + "_$longitude" WorkManager.getInstance(this) .enqueueUniquePeriodicWork( uniqueTag, ExistingPeriodicWorkPolicy.REPLACE, request ) }
  76. Box( modifier = GlanceModifier.fillMaxSize() .background(ImageProvider(resId = R.drawable.shape_widget_small)) .appWidgetBackground() .clickable( onClick

    = actionStartActivity( activity = MainActivity::class.java, parameters = actionParametersOf( ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LATITUDE) to state.data.latitude, ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LONGITUDE) to state.data.longitude, ActionParameters.Key<String>(WidgetConst.WIDGET_NAME_KEY) to (state.data.address ?: "") ) ) ),
  77. Box( modifier = GlanceModifier.fillMaxSize() .background(ImageProvider(resId = R.drawable.shape_widget_small)) .appWidgetBackground() .clickable( onClick

    = actionStartActivity( activity = MainActivity::class.java, parameters = actionParametersOf( ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LATITUDE) to state.data.latitude, ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LONGITUDE) to state.data.longitude, ActionParameters.Key<String>(WidgetConst.WIDGET_NAME_KEY) to (state.data.address ?: "") ) ) ),
  78. Box( modifier = GlanceModifier.fillMaxSize() .background(ImageProvider(resId = R.drawable.shape_widget_small)) .appWidgetBackground() .clickable( onClick

    = actionStartActivity( activity = MainActivity::class.java, parameters = actionParametersOf( ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LATITUDE) to state.data.latitude, ActionParameters.Key<Double>(WidgetConst.LOCATION_WIDGET_LONGITUDE) to state.data.longitude, ActionParameters.Key<String>(WidgetConst.WIDGET_NAME_KEY) to (state.data.address ?: "") ) ) ),
  79. initial_widget_layout.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="12dp"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:layout_gravity="center_horizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Loading data..." /> </LinearLayout>
  80. Thoughts and conclusion • We needed this! • It works

    on composable runtime • Supports android 12 features • It is in alpha3 • Updates are slow • No o ffi cial guidelines regarding state update, etc
  81. Questions? Piotr Prus Mobile Tech Lead @Airly GDG 3City Organizer

    https://github.com/PiotrPrus/WeatherGlanceWidget