Введение

В предыдущей статье мы создали простой статический виджет.

В этой статье мы разберем, как добавить конфигурационную activity, в которой мы сможем включить или отключить динамические цвета в виджете.

Цель

C Android 12 (API 31) появилась поддержка динамических цветов. Если мы добавим эти цвета из GlanceTheme - система сама подставит цвета. А что, если нам нужны цвета нашего приложения?

Мы сделаем следующее:

  • Если у нас девайс версии Android 11 (API 30) или меньше, то на конфигурационной activity простой текст. Для виджета используются цвета приложения;

  • Если у нас девайс версии Android 12 (API 31) или больше, то мы добавляем Switch, в котором мы можем включить или выключить отображение динамических цветов для виджета.

Configuration Activity

Это обычная activity, которая просто должна сохранять настройки виджета:

class AppWidgetConfigurationActivity : ComponentActivity() {
   private var appWidgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID

  ...
  
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       initialize()
       val isDynamicColorsAvailable = DynamicColors.isDynamicColorAvailable()

       ...
   }

   private fun initialize() {
       appWidgetId = intent?.extras?.getInt(
           AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
       ) ?: AppWidgetManager.INVALID_APPWIDGET_ID

       if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
           finish()
       }

       lifecycleScope.launch {
           val glanceId = getGlanceId()
           val dataStore =
               getAppWidgetState(applicationContext, PreferencesGlanceStateDefinition, glanceId)

           val isDynamicColorsChecked = dataStore.getSavedDynamicColorsChecked()
           viewModel.setDynamicColorsChecked(isDynamicColorsChecked)
       }
   }

   private fun saveWidgetState(dynamicColorChecked: Boolean) =
       lifecycleScope.launch(Dispatchers.IO) {
           val glanceId = getGlanceId()
           updateAppWidgetState(applicationContext, glanceId) { prefs ->
               prefs.saveConfigurations(dynamicColorChecked)
           }
           AppWidget().update(applicationContext, glanceId)

           closeConfigurationActivity()
       }

   private fun closeConfigurationActivity() {
       val result = Intent().apply {
           putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
       }
       setResult(RESULT_OK, result)
       finish()
   }

   private fun getGlanceId(): GlanceId =
       GlanceAppWidgetManager(applicationContext).getGlanceIdBy(appWidgetId)
}

Тут стоит объяснить несколько моментов:

  • Эта activity запускается при первом добавлении виджета на экран. Если мы хотим иметь возможность открывать этот экран сколько удобно раз, то в файл конфигурации виджета (res/xml/app_widget_provider.xml) необходимо добавить следующий атрибут:

<appwidget-provider
  ...
  android:configure="klimov.example.widget.widgets.configuration.AppWidgetConfigurationActivity" />
  • Каждый виджет имеет свой appWidgetId: Int. Он создается, когда мы добавляем виджет на экран. Если мы открывает activity через виджет, то в Intent этой activity приходит EXTRA_APPWIDGET_ID :

appWidgetId = intent?.extras?.getInt(
    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
  • При создании Glance виджета используется GlanceId, который можно получить вот так:

   private fun getGlanceId(): GlanceId =
       GlanceAppWidgetManager(applicationContext).getGlanceIdBy(appWidgetId)
  • Так же добавим эту activity в Manifest.xml, с определенным intent-filter

<intent-filter>
   <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
  • При создании нового экземпляра Glance виджета для него создается собственное локальное хранилище. Для изменяемого хранилища мы используем такой доступ:

updateAppWidgetState(applicationContext, glanceId) { prefs ->
    prefs.saveConfigurations(dynamicColorChecked)
}

Что такое saveConfigurations? Это собственный extensions, который можно написать:

import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey

private val KEY_DYNAMIC_COLORS_CHECKED = booleanPreferencesKey("widget_dynamic_colors_checked")

fun MutablePreferences.saveConfigurations(
   dynamicColorChecked: Boolean
) {
   this[KEY_DYNAMIC_COLORS_CHECKED] = dynamicColorChecked
}

fun Preferences.getSavedDynamicColorsChecked(): Boolean = this[KEY_DYNAMIC_COLORS_CHECKED] ?: true
  • Важный код, который позволит обновить конкретный виджет:

AppWidget().update(applicationContext, glanceId)

Но есть нюанс. Glance виджет обновится, если в ЕГО внутреннем хранилище что-то изменилось. То есть сторонние префы, бд или любой другой способ не подходит.

  • Для чего нам closeConfigurationActivity()? Чтобы система поняла, что конфигурационная Activity отработала корректно и можно применить изменения к виджету. В противном случае, система просто не добавит виджет после activity, потому что сочтет, что произошел сбой.

Остальные моменты Activty

Все остальное неважно в рамках данной статьи. XML, compose, какая архитектура и технологии - все это для виджета уже будет неважно. Это простая acitivity и ты можешь писать там что угодно.

Можно добавить любые настройки, которые придумаешь и сохранять их в локальный DataStore.

Лично я добавил просто Switch, который отвечает - используем мы динамические цвета или нет, а так же кнопку Save для сохранения настроек виджета..

Результаты Configuration Activity

У нас есть Configuration activity, с любой версткой и логикой. Разница только в том, что данная activity прописана в Manifest.xml с особым intent-filter и добавлена в @xml/app_widget_provider.

Все результаты сохраняются в локальное хранилище виджета по его GlanceId. Теперь время применить эти настройки в самом виджете.

Применение настроек в виджете

Давай теперь вытащим значение из настроек в AppWidget.kt:

private fun isDynamicColorsAvailable(prefs: Preferences): Boolean {
   val dynamicColorsChecked = prefs.getSavedDynamicColorsChecked()
   return dynamicColorsChecked && DynamicColors.isDynamicColorAvailable()
}

И сам prefs:

val prefs = currentState<Preferences>()

Отлично. Теперь остается только применить цвета в нашей верстке в зависимости от значения isDynamicColorsAvailable.

Не буду прикреплять весь код, потому что по большей части он везде будет одинаковым.

Допустим, мы хотим задать цвет тексту, тогда наш цвет будет определяться следующей логикой:

val onSurfaceColor = when (isDynamicColorAvailable) {
   true -> GlanceTheme.colors.onSurface
   false -> getPrimaryLabelColorProvider()
}

Для динамической темы мы просто возьмем цвет GlanceTheme.colors.onSurface, который система нам сама отдаст. В случае, если нам нужны наши цвета, то getPrimaryLabelColorProvider будет иметь примерно следующий вид:

import androidx.glance.color.ColorProvider
import androidx.glance.unit.ColorProvider

fun getPrimaryLabelColorProvider(): ColorProvider {
   val light = Color.Black

   return when (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
       true -> ColorProvider(day = light, night = Color.White)
       false -> ColorProvider(light)
   }
}

Здесь тоже есть свое условие, потому что Dark mode в Android появился с 10 версии. Для версий ниже мы просто сделаем виджет в светлой теме.

Итог

За две статьи мы добавили виджет в приложение, создали и подключили конфигурационное activity, которое позволяет применять и сохранять настройки для нашего виджета.

От автора

Код не претендует на истину в последней инстанции или текущее решение не единственно верное. Местами код упрощен, местами некоторые базовые вещи опущены. Стараюсь делать акцент на теме статьи.

В будущем еще добавим:

  • Обновление виджета при помощи WorkManager;

  • Обработка нажатия на элементы. Glance не так просто в этом, как кажется.

Github

https://github.com/Klimov-ilya/Widget/tree/part_2

Статьи по теме

  1. Создание простого виджета

  2. Добавление конфигурационной activity (Ты сейчас здесь)

Комментарии (0)