Criar um widget de app com o Glance

As seções a seguir descrevem como criar um widget de app básico com o Glance.

Declarar o AppWidget no manifesto

Depois de concluir as etapas de configuração, declare o AppWidget e os metadados dele no app.

  1. Estenda o receptor AppWidget de GlanceAppWidgetReceiver:

    class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
        override val glanceAppWidget: GlanceAppWidget = TODO("Create GlanceAppWidget")
    }

  2. Registre o provedor do widget do app no arquivo AndroidManifest.xml e no arquivo de metadados associado:

        <receiver android:name=".glance.MyReceiver"
        android:exported="true">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/my_app_widget_info" />
    </receiver>
    

Adicionar os metadados AppWidgetProviderInfo

Em seguida, siga o guia Criar um widget para criar e definir as informações do widget do app no arquivo @xml/my_app_widget_info.

A única diferença para o Resumo é que não há XML initialLayout, mas você precisa definir um. É possível usar o layout de carregamento predefinido fornecido na biblioteca:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/glance_default_loading_layout">
</appwidget-provider>

Declarar o XML AppWidgetProviderInfo

O objeto AppWidgetProviderInfo define as qualidades essenciais do widget. Defina o AppWidgetProviderInfo no arquivo de recursos de metadados XML (res/xml/my_app_widget_info.xml) dentro de um elemento <appwidget-provider>:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="120dp"
    android:updatePeriodMillis="86400000"
    android:description="@string/example_appwidget_description"
    android:previewLayout="@layout/example_appwidget_preview"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>

Atributos de dimensionamento de widgets

A tela inicial padrão posiciona os widgets na janela com base em uma grade de células que têm altura e largura definidas. A maioria das telas iniciais só permite que os widgets tenham tamanhos que sejam múltiplos inteiros das células da grade, por exemplo, duas células na horizontal por três células na vertical.

Os atributos de dimensionamento do widget permitem especificar um tamanho padrão para o widget e fornecer limites inferiores e superiores para o tamanho dele. Nesse contexto, o tamanho padrão de um widget é o tamanho que ele assume quando é adicionado pela primeira vez à tela inicial.

A tabela a seguir descreve os atributos de <appwidget-provider> relacionados ao dimensionamento de widgets:

Atributos e descrição
targetCellWidth e targetCellHeight (Android 12), minWidth e minHeight
  • A partir do Android 12, os atributos targetCellWidth e targetCellHeight especificam o tamanho padrão do widget em termos de células da grade. Esses atributos são ignorados no Android 11 e versões anteriores e podem ser ignorados se a tela inicial não oferecer suporte a um layout baseado em grade.
  • Os atributos minWidth e minHeight especificam o tamanho padrão do widget em dp. Se os valores de largura ou altura mínima de um widget não corresponderem às dimensões das células, eles serão arredondados para o tamanho de célula mais próximo.
Recomendamos especificar os dois conjuntos de atributos, targetCellWidth e targetCellHeight, e minWidth e minHeight, para que o app possa usar minWidth e minHeight se o dispositivo do usuário não for compatível com targetCellWidth e targetCellHeight. Se houver suporte, os atributos targetCellWidth e targetCellHeight vão ter precedência sobre os atributos minWidth e minHeight.
minResizeWidth e minResizeHeight Especifique o tamanho mínimo absoluto do widget. Esses valores especificam o tamanho abaixo do qual o widget fica ilegível ou inutilizável. O uso desses atributos permite que o usuário redimensione o widget para um tamanho menor que o padrão. O atributo minResizeWidth será ignorado se for maior que minWidth ou se o redimensionamento horizontal não estiver ativado. Consulte resizeMode. Da mesma forma, o atributo minResizeHeight será ignorado se for maior que minHeight ou se o redimensionamento vertical não estiver ativado.
maxResizeWidth e maxResizeHeight Especifique o tamanho máximo recomendado do widget. Se os valores não forem um múltiplo das dimensões da célula da grade, eles serão arredondados para o tamanho de célula mais próximo. O atributo maxResizeWidth será ignorado se for menor que minWidth ou se o redimensionamento horizontal não estiver ativado. Consulte resizeMode. Da mesma forma, o atributo maxResizeHeight será ignorado se for menor que minHeight ou se o redimensionamento vertical não estiver ativado. Introduzido no Android 12.
resizeMode Especifica as regras para redimensionar um widget. Você pode usar esse atributo para tornar os widgets da tela inicial redimensionáveis horizontalmente, verticalmente ou em ambos os eixos. Os usuários tocam e mantêm pressionado um widget para mostrar as alças de redimensionamento e, em seguida, arrastam as alças horizontais ou verticais para mudar o tamanho dele no layout de grade. Os valores do atributo resizeMode incluem horizontal, vertical e none. Para declarar um widget como redimensionável horizontal e verticalmente, use horizontal|vertical.

Exemplo

Para ilustrar como os atributos na tabela anterior afetam o dimensionamento do widget, considere as seguintes especificações:

  • Uma célula da grade tem 30 dp de largura e 50 dp de altura.
  • A seguinte especificação de atributo é fornecida:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="80dp"
    android:minHeight="80dp"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:minResizeWidth="40dp"
    android:minResizeHeight="40dp"
    android:maxResizeWidth="120dp"
    android:maxResizeHeight="120dp"
    android:resizeMode="horizontal|vertical" />

A partir do Android 12:

Use os atributos targetCellWidth e targetCellHeight como o tamanho padrão do widget.

O tamanho padrão do widget é 2x2. O widget pode ser redimensionado para 2x1 ou até 4x3.

Android 11 e versões anteriores:

Use os atributos minWidth e minHeight para calcular o tamanho padrão do widget.

A largura padrão = Math.ceil(80 / 30) = 3

A altura padrão = Math.ceil(80 / 50) = 2

O tamanho padrão do widget é 3x2. O widget pode ser redimensionado para 2x1 ou até tela cheia.

Outros atributos de widget

A tabela a seguir descreve os atributos de <appwidget-provider> relacionados a qualidades que não sejam o dimensionamento do widget.

Atributos e descrição
updatePeriodMillis Define a frequência com que o framework do widget solicita uma atualização do GlanceAppWidgetReceiver chamando o método de callback onUpdate(). Recomendamos atualizar o mínimo possível, no máximo uma vez por hora, para economizar bateria. Para mais detalhes, consulte a seção Quando atualizar widgets no gerenciamento de estado do Resumo.
initialLayout Aponta para o recurso de layout que define o layout de carregamento do widget antes que as composições da IU do Resumo sejam renderizadas. Você pode usar o layout de carregamento predefinido fornecido na biblioteca: @layout/glance_default_loading_layout.
configure Define a atividade de configuração que é iniciada quando o usuário adiciona o widget. Consulte o guia Permitir que os usuários configurem widgets de apps.
description Especifica a descrição que o seletor de widgets vai mostrar para seu widget. Introduzido no Android 12.
previewLayout (Android 12) e previewImage (Android 11 e versões anteriores)
  • No Android 12, o atributo previewLayout especifica uma prévia escalonável, que é fornecida como um layout XML definido para o tamanho padrão do widget. O ideal é que isso aponte para um mapeamento XML estático que corresponda ao layout do design.
  • No Android 11 ou versões anteriores, o atributo previewImage especifica uma captura de tela de imagem estática que pode ser mostrada do widget, que aparece no seletor de widgets.
Recomendamos especificar os dois para que o app funcione sem problemas em plataformas mais antigas. Para plataformas mais recentes (Android 15+), é possível definir prévias geradas em tempo real no Kotlin usando `GlanceAppWidget.providePreview`. Consulte o guia de prévias geradas.
autoAdvanceViewId Especifica o ID da subvisualização do widget que é avançada automaticamente pelo host do widget.
widgetCategory Declara se o widget pode ser mostrado na tela inicial (home_screen), na tela de bloqueio (keyguard) ou em ambas. Para o Android 5.0 e versões mais recentes, apenas home_screen é válido.
widgetFeatures Declara os recursos compatíveis com o widget. Por exemplo, se a configuração do widget for opcional, especifique configuration_optional e reconfigurable.

Definir GlanceAppWidget

  1. Crie uma classe que estenda GlanceAppWidget e substitua o método provideGlance. Esse é o método em que você pode carregar os dados necessários para renderizar o widget:

    class MyAppWidget : GlanceAppWidget() {
    
        override suspend fun provideGlance(context: Context, id: GlanceId) {
    
            // In this method, load data needed to render the AppWidget.
            // Use `withContext` to switch to another thread for long running
            // operations.
    
            provideContent {
                // create your AppWidget here
                Text("Hello World")
            }
        }
    }

  2. Instancie-o no glanceAppWidget do seu GlanceAppWidgetReceiver:

    class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
    
        // Let MyAppWidgetReceiver know which GlanceAppWidget to use
        override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
    }

Você configurou um AppWidget usando o Resumo.

Gerenciar a memória de imagens de widgets em buckets de tamanho

Quando o widget exibir bitmaps ou URIs de imagem content://, mantenha a memória total serializada da imagem RemoteViews dentro do limite do sistema (6 * width * height bytes, aplicados com um IllegalArgumentException no Android 17 e versões mais recentes) fazendo o seguinte:

  • Decodificar bitmaps compartilhados antes de provideContent:se o widget usar SizeMode.Responsive ou SizeMode.Exact, o Resumo vai executar a lambda provideContent uma vez para cada bucket DpSize. Decodificar um Bitmap dentro de provideContent aloca uma instância de Bitmap separada para cada bucket, multiplicando o uso da memória em RemoteViews.mBitmapCache. Decodifique bitmaps com redução de resolução (Bitmap.Config.RGB_565 para imagens opacas) uma vez dentro de provideGlance antes de provideContent para que todos os buckets compartilhem uma única referência de Bitmap.
  • Use a importação ImageProvider correspondente:o Jetpack Glance oferece duas sobrecargas ImageProvider distintas em dois pacotes:

    • Use import androidx.glance.ImageProvider ao transmitir um ImageProvider(bitmap: Bitmap) na memória ou um recurso drawable ImageProvider(resId: Int).
    • Use import androidx.glance.appwidget.ImageProvider ao transmitir um URI ImageProvider(uri: Uri). Ao exibir arquivos em cache por um URI FileProvider, conceda Intent.FLAG_GRANT_READ_URI_PERMISSION a todos os pacotes de inicialização Intent.CATEGORY_HOME instalados para que o host possa renderizar a imagem.

Usar a classe GlanceAppWidgetReceiver para processar transmissões de widgets

O widget de coordenadas GlanceAppWidgetReceiver transmite e atualiza o estado da plataforma estendendo o AppWidgetProvider. Ele recebe eventos da plataforma quando o widget é atualizado, excluído, ativado ou desativado, traduzindo-os em solicitações de ciclo de vida do Compose.

Declarar um widget no manifesto

Declare a subclasse da classe GlanceAppWidgetReceiver como um broadcast receiver no arquivo AndroidManifest.xml:

<receiver android:name="MyReceiver"
          android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/my_app_widget_info" />
</receiver>

O elemento <receiver> requer o atributo android:name, que especifica a classe do receptor. O receptor precisa aceitar a ação de transmissão ACTION_APPWIDGET_UPDATE dentro do <intent-filter>.

O elemento <meta-data> precisa identificar o nome como android.appwidget.provider, e o atributo android:resource precisa apontar para o recurso de metadados XML AppWidgetProviderInfo (@xml/my_app_widget_info).

Implementar a classe GlanceAppWidgetReceiver

No Resumo, você estende GlanceAppWidgetReceiver em vez de AppWidgetProvider diretamente. Implemente-o vinculando o receptor à instância GlanceAppWidget. Os principais callbacks disponíveis em GlanceAppWidgetReceiver funcionam da seguinte maneira:

  • onUpdate(): substituído automaticamente pelo Resumo para executar atualizações de composição. Se você substituir onUpdate manualmente, precisará chamar super.onUpdate para permitir que o Resumo inicie as linhas de execução de composição.
  • onAppWidgetOptionsChanged(): chamado quando o widget é colocado ou redimensionado pela primeira vez. O Resumo lê os itens do pacote de opções em segundo plano para que o layout se ajuste perfeitamente com base nas dimensões do ambiente de execução.
  • onDeleted(Context, IntArray): invocado sempre que uma instância de widget específica é excluída pelo usuário.
  • onEnabled(Context): acionado quando a primeira instância do widget é criada. Excelente para executar migrações globais.
  • onDisabled(Context): chamado quando a última instância ativa do provedor é removida.
  • onReceive(Context, Intent): intercepta todas as transmissões da plataforma antes de métodos de callback específicos. É preciso garantir que qualquer lógica de receptor personalizada que você escreva chame super.onReceive(context, intent) e nunca chame goAsync, já que o Resumo encaminha o trabalho automaticamente de forma assíncrona.

Receber intents de transmissão de widget

Nos bastidores, o GlanceAppWidgetReceiver filtra e processa as seguintes intents de transmissão de widgets da plataforma:

Criar interface

O snippet a seguir demonstra como criar a interface:

/* Import Glance Composables
 In the event there is a name clash with the Compose classes of the same name,
 you may rename the imports per https://kotlinlang.org/docs/packages.html#imports
 using the `as` keyword.

import androidx.glance.Button
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.text.Text
*/
class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // Load data needed to render the AppWidget.
        // Use `withContext` to switch to another thread for long running
        // operations.

        provideContent {
            // create your AppWidget here
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        Column(
            modifier = GlanceModifier.fillMaxSize(),
            verticalAlignment = Alignment.Top,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button(
                    text = "Home",
                    onClick = actionStartActivity<MyActivity>()
                )
                Button(
                    text = "Work",
                    onClick = actionStartActivity<MyActivity>()
                )
            }
        }
    }
}

O exemplo de código anterior faz o seguinte:

  • No Column de nível superior, os itens são colocados verticalmente um após o outro.
  • A Column expande o tamanho para corresponder ao espaço disponível (com o GlanceModifier), alinha o conteúdo à parte de cima (verticalAlignment) e o centraliza horizontalmente (horizontalAlignment).
  • O conteúdo do Column é definido usando o lambda. A ordem é importante.
    • O primeiro item na Column é um componente Text com 12.dp de padding.
    • O segundo item é um Row, em que os itens são colocados horizontalmente, um após o outro, com dois Buttons centralizados horizontalmente (horizontalAlignment). A exibição final depende do espaço disponível. A imagem abaixo é um exemplo de como isso pode ser feito:
destination_widget
Figura 1. Exemplo de interface.

É possível mudar os valores de alinhamento ou aplicar valores de modificador diferentes (como preenchimento) para mudar o posicionamento e o tamanho dos componentes. Consulte a documentação de referência para ver uma lista completa de componentes, parâmetros e modificadores disponíveis para cada classe.

Implementar cantos arredondados

O Android 12 introduz parâmetros do sistema para personalizar dinamicamente os raios dos cantos dos widgets do app:

  • system_app_widget_background_radius: especifica o raio do canto do contêiner de plano de fundo do widget (nunca maior que 28 dp).
  • Raio interno:para evitar o corte de conteúdo, calcule um raio proporcional para o conteúdo interno com base no contorno do plano de fundo do sistema: systemRadiusValue - widgetPadding

No Resumo, é possível aplicar propriedades de dimensionamento de raio de canto dinamicamente na composição usando GlanceModifier.cornerRadius(android.R.dimen.system_app_widget_background_radius).

Para compatibilidade com versões anteriores em dispositivos com o Android 11 (nível 30 da API) ou versões anteriores, implemente atributos personalizados e substitutos de recursos de tema personalizados:

  • /values/attrs.xml

    <resources>
    <attr name="backgroundRadius" format="dimension" />
    </resources>
    
  • /values/styles.xml

    <resources>
    <style name="MyWidgetTheme">
      <item name="backgroundRadius">@dimen/my_background_radius_dimen</item>
    </style>
    </resources>
    
  • /values-31/styles.xml

    <resources>
    <style name="MyWidgetTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
      <item name="backgroundRadius">@android:dimen/system_app_widget_background_radius</item>
    </style>
    </resources>
    
  • /drawable/my_widget_background.xml

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="?attr/backgroundRadius" />
    </shape>