Cómo compilar una IU con Glance

En esta página, se describe cómo controlar los tamaños y proporcionar diseños flexibles y responsivos con Glance, usando los componentes existentes de Glance.

Usa Box, Column y Row

Glance tiene tres diseños componibles principales:

  • Box: Coloca elementos uno encima del otro. Se traduce en un RelativeLayout.

  • Column: Coloca elementos uno después del otro en el eje vertical. Se traduce en un LinearLayout con orientación vertical.

  • Row: Coloca elementos uno después del otro en el eje horizontal. Se traduce en un LinearLayout con orientación horizontal.

Glance admite objetos Scaffold. Coloca tus elementos componibles Column, Row y Box dentro de un objeto Scaffold determinado.

Diseño de columna, fila y caja.
Figura 1. Ejemplos de diseños con Column, Row y Box

Cada uno de estos elementos componibles te permite definir las alineaciones verticales y horizontales de su contenido, y las restricciones de ancho, alto, peso o padding con modificadores. Además, cada elemento secundario puede definir su modificador para cambiar el espacio y la ubicación dentro del elemento superior.

En el siguiente ejemplo, se muestra cómo crear un Row que distribuye de manera uniforme sus elementos secundarios de forma horizontal, como se muestra en la Figura 1:

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

El Row ocupa el ancho máximo disponible y, como cada elemento secundario tiene el mismo peso, comparten de manera uniforme el espacio disponible. Puedes definir diferentes pesos, tamaños, paddings o alineaciones para adaptar los diseños a tus necesidades.

Usa diseños desplazables

Otra forma de proporcionar contenido responsivo es hacerlo desplazable. Esto es posible con el elemento componible LazyColumn. Este elemento componible te permite definir un conjunto de elementos que se mostrarán dentro de un contenedor desplazable en el widget de la app.

En los siguientes fragmentos, se muestran diferentes formas de definir elementos dentro de LazyColumn.

Puedes proporcionar la cantidad de elementos:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

Proporciona elementos individuales:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

Proporciona una lista o un array de elementos:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

También puedes usar una combinación de los ejemplos anteriores:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

Ten en cuenta que el fragmento anterior no especifica el itemId. Especificar el itemId ayuda a mejorar el rendimiento y mantener la posición de desplazamiento a través de las actualizaciones de la lista y appWidget desde Android 12 en adelante (por ejemplo, cuando se agregan o quitan elementos de la lista). En el siguiente ejemplo, se muestra cómo especificar un itemId:

items(items = peopleList, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Desplazamiento rápido

El desplazamiento rápido es una animación que permite que el contenido desplazable se ajuste a la parte superior del contenedor del widget.

Video 1: En la izquierda, se muestra un elemento de lista que no se ajusta en su lugar mientras se desplaza, mientras que en la derecha sí lo hace.


Para implementar el desplazamiento rápido, asegúrate de cumplir con las siguientes condiciones:

  • Actualiza tu dependencia de Glance a 1.3.0-alpha02 o una versión posterior.
  • Configura tu compileSdk en 37 o una versión posterior, ya que el desplazamiento rápido es compatible con dispositivos que ejecutan Android 17 y versiones posteriores.
  • Configura tu LazyColumn con VerticalScrollMode. Si el dispositivo admite el desplazamiento rápido, usa SnapScrollMatchHeight. De lo contrario, usa Normal.

Si usas el desplazamiento rápido con imágenes, consulta el diseño canónico de imagen de sangrado completo.

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

Define el SizeMode

Los tamaños de AppWidget pueden diferir según el dispositivo, la elección del usuario o el selector, por lo que es importante proporcionar diseños flexibles como se describe en la página Proporciona diseños de widgets flexibles. Glance simplifica esto con la definición SizeMode y el valor LocalSize. En las siguientes secciones, se describen los tres modos.

SizeMode.Single

SizeMode.Single es el modo predeterminado. Indica que solo se proporciona un tipo de contenido; es decir, incluso si cambia el tamaño disponible de AppWidget, no se cambia el tamaño del contenido.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

Cuando uses este modo, asegúrate de lo siguiente:

  • Los valores de metadatos de tamaño mínimo y máximo metadata values se definen correctamente según el tamaño del contenido.
  • El contenido es lo suficientemente flexible dentro del rango de tamaño esperado.

En general, debes usar este modo cuando se cumpla alguna de las siguientes condiciones:

a) El AppWidget tiene un tamaño fijo o b) no cambia su contenido cuando se cambia de tamaño.

SizeMode.Responsive

Este modo es el equivalente a proporcionar diseños responsivos, lo que permite que GlanceAppWidget defina un conjunto de diseños responsivos delimitados por tamaños específicos. Para cada tamaño definido, el contenido se crea y se asigna al tamaño específico cuando se crea o actualiza el AppWidget. Luego, el sistema selecciona el que mejor se ajuste según el tamaño disponible.

Por ejemplo, en nuestro destino AppWidget, puedes definir tres tamaños y su contenido:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

En el ejemplo anterior, se llama al método provideContent tres veces y se asigna al tamaño definido.

  • En la primera llamada, el tamaño se evalúa como 100x100. El contenido no incluye el botón adicional ni los textos superior e inferior.
  • En la segunda llamada, el tamaño se evalúa como 250x100. El contenido incluye el botón adicional, pero no los textos superior e inferior.
  • En la tercera llamada, el tamaño se evalúa como 250x250. El contenido incluye el botón adicional y ambos textos.

SizeMode.Responsive es una combinación de los otros dos modos y te permite definir contenido responsivo dentro de límites predefinidos. En general, este modo funciona mejor y permite transiciones más fluidas cuando se cambia el tamaño de AppWidget.

En la siguiente tabla, se muestra el valor del tamaño, según el SizeMode y el tamaño disponible de AppWidget:

Tamaño disponible 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Los valores exactos son solo para fines de demostración.

SizeMode.Exact

SizeMode.Exact es el equivalente a proporcionar diseños exactos, que solicita el contenido de GlanceAppWidget cada vez que cambia el tamaño disponible de AppWidget (por ejemplo, cuando el usuario cambia el tamaño de AppWidget en la pantalla principal).

Por ejemplo, en el widget de destino, se puede agregar un botón adicional si el ancho disponible es mayor que un valor determinado.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

Este modo proporciona más flexibilidad que los demás, pero tiene algunas advertencias:

  • El AppWidget se debe volver a crear por completo cada vez que cambia el tamaño. Esto puede generar problemas de rendimiento y saltos de la IU cuando el contenido es complejo.
  • El tamaño disponible puede diferir según la implementación del selector. Por ejemplo, si el selector no proporciona la lista de tamaños, se usa el tamaño mínimo posible.
  • En dispositivos anteriores a Android 12, es posible que la lógica de cálculo de tamaño no funcione en todas las situaciones.

En general, debes usar este modo si no se puede usar SizeMode.Responsive (es decir, no es factible un pequeño conjunto de diseños responsivos).

Accede a los recursos

Usa LocalContext.current para acceder a cualquier recurso de Android, como se muestra en el siguiente ejemplo:

LocalContext.current.getString(R.string.glance_title)

Te recomendamos que proporciones IDs de recursos directamente para reducir el tamaño del objeto final RemoteViews y habilitar recursos dinámicos, como colores dinámicos.

Los elementos componibles y los métodos aceptan recursos con un "proveedor", como ImageProvider, o con un método de sobrecarga como GlanceModifier.background(R.color.blue). Por ejemplo:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

Controla el texto

Glance 1.1.0 incluye una API para configurar tus estilos de texto. Configura los estilos de texto con los atributos fontSize, fontWeight o fontFamily de la clase TextStyle.

fontFamily admite todas las fuentes del sistema, como se muestra en el siguiente ejemplo, pero no se admiten fuentes personalizadas en las apps:

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

Agrega botones compuestos

Los botones compuestos se introdujeron en Android 12. Glance admite la retrocompatibilidad para los siguientes tipos de botones compuestos:

Cada uno de estos botones compuestos muestra una vista en la que se puede hacer clic que representa el estado "marcado".

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

Cuando cambia el estado, se activa la expresión lambda proporcionada. Puedes almacenar el estado marcado, como se muestra en el siguiente ejemplo:

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

También puedes proporcionar el atributo colors a CheckBox, Switch y RadioButton para personalizar sus colores:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)

Componentes adicionales

Glance 1.1.0 incluye el lanzamiento de componentes adicionales, como se describe en la siguiente tabla:

Nombre Imagen Vínculo de referencia Notas adicionales
Botón con relleno alt_text Componente
Botones con contorno alt_text Componente
Botones de ícono alt_text Componente Principal / secundario / solo ícono
Barra de título alt_text Componente
Scaffold Scaffold y la barra de título están en la misma demostración.

Para obtener más información sobre los detalles de diseño, consulta los diseños de componentes en este kit de diseño en Figma.

Para obtener más información sobre los diseños canónicos, visita Diseños canónicos de widgets.