Índice de contenido
- El Enfoque Moderno: Jetpack Compose
- Dibujando con Canvas en Jetpack Compose
- Dibujando nuestra primera figura con Canvas: una cara
- Conceptos Fundamentales: Dimensiones y Sistema de Coordenadas
- Dibujando nuestra primera figura
- 1. El Círculo Base
- 2. Contornos con Stroke
- Transformaciones: Offset, Translate y Arcos
- El Arco (drawArc)
- Ejemplo Real: Barras de Estadísticas con drawRoundRect
- Canvas en Android — Forma Legacy: el método onDraw()
- El método onDraw()
- Dibujando una figura:
- Asociando el Canvas en la actividad
- ¿Dibujar gráficos dentro de los layouts o directamente en el Canvas?
- Dibujando un SeekBar Personalizado en Android con Canvas
- Definiendo las bases: la estructura de la actividad
- La clase SeekBarView: dibujando el SeekBar con Canvas
En este artículo aprenderás a dibujar formas geométricas y figuras personalizadas en Android usando la API de Canvas. Veremos primero el enfoque moderno con Jetpack Compose, que utiliza funciones @Composable para un desarrollo de UI más rápido, declarativo y sencillo; y al final repasaremos la forma tradicional (ahora considerada «Legacy») basada en el sistema de View y la clase Canvas. Aunque este enfoque clásico sigue siendo funcional y útil para proyectos más antiguos, el desarrollo moderno de Android ha evolucionado significativamente hacia Compose.
Un Canvas no es más que una superficie o lienzo en la que puedes dibujar. Con las primitivas que ofrece la API de Canvas en Android es posible trazar líneas, círculos, óvalos, cuadrados, rectángulos e incluso texto, además de controlar el color, el grosor del trazado y el tamaño de cada figura. En esta entrada damos los primeros pasos con la clase Canvas en Android, desde lo más básico hasta un ejemplo práctico con barras de estadísticas animadas.
En una entrega anterior vimos cómo realizar una conexión a Internet en Android con Retrofit. Al final de este artículo aprovecharemos ese conocimiento para mostrar una barra de energía usando Canvas.
El Enfoque Moderno: Jetpack Compose
La manera en que se construyen las interfaces de usuario en Android ha evolucionado considerablemente. El enfoque tradicional basado en View y archivos XML sigue vigente en millones de apps, pero la nueva forma recomendada por Google es Jetpack Compose: un framework de UI declarativo que elimina la necesidad de inflar layouts en XML y permite describir la interfaz directamente con código Kotlin.
Dibujando con Canvas en Jetpack Compose
El concepto de Canvas sigue existiendo en Jetpack Compose, pero se utiliza de una manera más directa y declarativa dentro de una función @Composable. Ya no es necesario crear una clase que extienda de View ni sobreescribir el método onDraw. En su lugar, usas el composable Canvas directamente en tu árbol de UI, lo que lo hace mucho más intuitivo y fácil de mantener.
Por ejemplo, para dibujar un círculo, simplemente usamos el composable Canvas y llamamos a la función drawCircle:
@Composable
fun MiCirculo() {
Canvas(modifier = Modifier.fillMaxSize()) {
val centro = Offset(x = size.width / 2, y = size.height / 2)
drawCircle(
color = Color.Blue,
radius = 100f,
center = centro
)
}
}Como puedes ver, el código es conciso y se integra de forma natural con el resto de la UI declarativa de Compose. Dentro del bloque lambda del composable Canvas, tienes acceso a un DrawScope que expone propiedades como size (dimensiones totales del canvas) y center (el punto medio automático), así como todas las funciones de dibujo: drawCircle, drawRect, drawArc, drawLine, entre otras. Este es el estándar actual para el desarrollo de UI en Android.
Dibujando nuestra primera figura con Canvas: una cara
Vamos a aprender las bases para trabajar con Canvas en Android usando composables. Lo primero que debes saber es que Canvas no está pensado para reemplazar figuras estáticas o sencillas que podrías lograr con otros componentes de Compose; su verdadero potencial reside en la creación de figuras complejas, gráficas de datos a medida y, sobre todo, en la implementación de animaciones avanzadas que serían muy difíciles de lograr con widgets estándar.
Aunque a veces la gente le huye por el componente matemático, en realidad se apoya en operaciones básicas que conocemos desde pequeños: círculos, cuadrados, rectángulos, triángulos, arcos y polígonos. Una vez que entiendes cómo posicionar y dimensionar cada primitiva, el resto es creatividad.
Conceptos Fundamentales: Dimensiones y Sistema de Coordenadas
Para dibujar en el Canvas de Android, el sistema de coordenadas tiene su origen (0, 0) en la esquina superior izquierda. El eje X crece hacia la derecha y el eje Y crece hacia abajo. Esto es diferente al plano cartesiano clásico y es importante tenerlo en cuenta al posicionar figuras.
Para dibujar, solo necesitamos recordar cómo se definen las dimensiones de cada figura básica:
- Círculo: Se define por su radio, que es la distancia desde el centro hasta el borde. Cuanto mayor sea este valor, más grande será el círculo. Se dibuja con
drawCircle(color, radius, center). - Cuadrado: Por definición, todos sus lados miden lo mismo (alto y ancho iguales). En Canvas, se dibuja con
drawRectodrawRoundRectpasando unSizecon valores iguales de ancho y alto. - Rectángulo: Varía tanto en su alto (
height) como en su ancho (width). Se dibuja condrawRectespecificando un objetoSizeo las coordenadastopLeftybottomRight.
El color es el aspecto más sencillo: si quieres un círculo amarillo, simplemente defines color = Color.Yellow al llamar la función de dibujo correspondiente.
Dibujando nuestra primera figura
El Canvas es, esencialmente, un lienzo en blanco a nuestra disposición. En el código, declaramos el composable Canvas, le asignamos un tamaño fijo (por ejemplo, 300.dp) y un padding para que las figuras no queden pegadas a los bordes del contenedor:
@Composable
fun MiDibujoPersonalizado() {
Canvas(
modifier = Modifier
.size(300.dp)
.padding(16.dp)
) {
// 1. Dibujar el rostro (Círculo amarillo)
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2,
center = center // 'center' es una propiedad automática del Canvas
)
// 2. Dibujar el contorno del rostro
drawCircle(
color = Color.Black,
radius = size.minDimension / 2,
style = Stroke(width = 5f) // Solo el borde
)
// 3. Dibujar los ojos
val eyeRadius = 20f
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x - 60f, center.y - 50f)
)
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x + 60f, center.y - 50f)
)
// 4. Dibujar la sonrisa (un arco)
drawArc(
color = Color.Black,
startAngle = 0f, // Empieza en la derecha (3 en un reloj)
sweepAngle = 180f, // Gira 180 grados hacia abajo
useCenter = false, // Si es true, cierra el arco hacia el centro (como un Pacman)
topLeft = Offset(center.x - 70f, center.y - 20f),
size = Size(140f, 100f),
style = Stroke(width = 10f, cap = StrokeCap.Round)
)
}
}1. El Círculo Base
Dentro del bloque Canvas, tienes acceso a propiedades automáticas del DrawScope como size (las dimensiones totales del lienzo) y center (el punto medio exacto). Esto evita tener que calcular manualmente el centro cada vez.
- Radio: Usamos
size.minDimension / 2para que el círculo ocupe exactamente el espacio disponible, tomando la dimensión menor entre el ancho y el alto para evitar que se salga del lienzo. - Posición: Usamos la propiedad
centerdelDrawScopepara ubicarlo automáticamente en el punto medio del lienzo.
@Composable
fun MiDibujoPersonalizado() {
Canvas(
modifier = Modifier
.size(300.dp)
.padding(16.dp)
) {
// 1. Dibujar el rostro (Círculo amarillo)
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2,
center = center // 'center' es una propiedad automática del Canvas
)
}2. Contornos con Stroke
Si queremos dibujar solo el borde de una figura sin rellenarla, utilizamos el estilo Stroke. Por defecto, todas las funciones de dibujo rellenan la figura con el color indicado (estilo Fill). Al pasar style = Stroke(width = Xf) como parámetro, le indicamos al Canvas que solo pinte el contorno con el grosor especificado en píxeles.
- Círculo Relleno: Comportamiento por defecto. No es necesario especificar el parámetro
style. - Círculo de Contorno: Al indicar
style = Stroke(width = 10f), el círculo será hueco y solo veremos su línea exterior con el grosor definido.
@Composable
fun MiDibujoPersonalizado() {
Canvas(
modifier = Modifier
.size(300.dp)
.padding(16.dp)
) {
// 1. Dibujar el rostro (Círculo amarillo)
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2,
center = center // 'center' es una propiedad automática del Canvas
)
// 2. Dibujar el contorno del rostro
drawCircle(
color = Color.Black,
radius = size.minDimension / 2,
style = Stroke(width = 5f) // Solo el borde
)
}Transformaciones: Offset, Translate y Arcos
Para mover figuras respecto a su posición predeterminada, utilizamos Offset, que define un desplazamiento en los ejes X e Y. Además, el DrawScope expone funciones de transformación más avanzadas como rotate() (rotación), scale() (escala) y translate() (traslación de origen), que funcionan como bloques dentro de los que las figuras se dibujan con el sistema de coordenadas modificado. Esto es muy similar a las transformaciones que se aplican en entornos de gráficos 2D o incluso en 3D como Blender: todo es cuestión de declarar tamaños, colores y posiciones.
@Composable
fun MiDibujoPersonalizado() {
Canvas(
modifier = Modifier
.size(300.dp)
.padding(16.dp)
) {
// 1. Dibujar el rostro (Círculo amarillo)
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2,
center = center // 'center' es una propiedad automática del Canvas
)
// 2. Dibujar el contorno del rostro
drawCircle(
color = Color.Black,
radius = size.minDimension / 2,
style = Stroke(width = 5f) // Solo el borde
)
// 3. Dibujar los ojos usando Offset para posicionarlos
val eyeRadius = 20f
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x - 60f, center.y - 50f)
)
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x + 60f, center.y - 50f)
)
}El Arco (drawArc)
Para formas curvas que no son círculos completos, como una sonrisa o una gráfica de torta, usamos drawArc. Es un poco más complejo porque requiere entender cómo se definen los ángulos en el sistema de Canvas (donde 0° apunta hacia la derecha, como las 3 en un reloj, y los ángulos crecen en sentido horario):
startAngle: Ángulo en grados donde comienza el arco. Por ejemplo,0fempieza en la posición de las 3 en un reloj.sweepAngle: Cuántos grados se extiende el arco desde elstartAngle. Por ejemplo,180fdibuja media circunferencia.useCenter: Si estrue, cierra el arco uniendo los extremos con el centro del rectángulo delimitador (creando una forma de «Pacman» o porción de tarta). Si esfalse, deja los extremos abiertos, mostrando solo la línea curva.
@Composable
fun MiDibujoPersonalizado() {
Canvas(
modifier = Modifier
.size(300.dp)
.padding(16.dp)
) {
// 1. Dibujar el rostro (Círculo amarillo)
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2,
center = center // 'center' es una propiedad automática del Canvas
)
// 2. Dibujar el contorno del rostro
drawCircle(
color = Color.Black,
radius = size.minDimension / 2,
style = Stroke(width = 5f) // Solo el borde
)
// 3. Dibujar los ojos
val eyeRadius = 20f
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x - 60f, center.y - 50f)
)
drawCircle(
color = Color.Black,
radius = eyeRadius,
center = Offset(center.x + 60f, center.y - 50f)
)
// 4. Dibujar la sonrisa (un arco)
drawArc(
color = Color.Black,
startAngle = 0f, // Empieza en la derecha (3 en un reloj)
sweepAngle = 180f, // Gira 180 grados hacia abajo
useCenter = false, // Si es true, cierra el arco hacia el centro (como un Pacman)
topLeft = Offset(center.x - 70f, center.y - 20f),
size = Size(140f, 100f),
style = Stroke(width = 10f, cap = StrokeCap.Round)
)
}
}Y con esto, obtenemos:

Ejemplo Real: Barras de Estadísticas con drawRoundRect
Para ver algo más útil que una cara estática, vamos a crear barras de progreso al estilo de las estadísticas de un Pokémon. Aquí ya no usamos círculos, sino rectángulos con esquinas redondeadas mediante la función drawRoundRect. Esta primitiva acepta un CornerRadius para suavizar las esquinas, lo que da un aspecto mucho más moderno y cuidado que un rectángulo plano.
El flujo lógico para construir cada barra es el siguiente:
- Fondo: Dibujamos un
drawRoundRectgris que ocupa todo el ancho disponible para representar el valor máximo posible (por ejemplo, 255 en las estadísticas de un Pokémon). - Progreso: Calculamos el ancho proporcional con la fórmula
(valorActual / valorMaximo) * anchoTotal. - Capa Superior: Dibujamos un segundo
drawRoundRectencima con el color correspondiente al tipo de estadística (rojo para ataque, azul para defensa, etc.), usando el ancho calculado en el paso anterior.
data class Stat(
val nombre: String,
val valor: Int,
val color: Color
)
val misStats = listOf(
Stat("HP", 45, Color.Green),
Stat("Ataque", 49, Color.Red),
Stat("Defensa", 49, Color.Blue),
Stat("Velocidad", 65, Color.Yellow)
)
***
@Composable
fun PokemonStatsChart(stats: List<Stat>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Estadísticas Base",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 16.dp)
)
stats.forEach { stat ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
// Nombre del stat
Text(text = stat.nombre, modifier = Modifier.width(80.dp))
// El Canvas donde dibujamos la barra
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
) {
val maxStat = 255f
// Calculamos el ancho proporcional al tamaño del Canvas
val progressWidth = (stat.valor / maxStat) * size.width
// 1. Dibujar el fondo de la barra (gris claro)
drawRoundRect(
color = Color.LightGray.copy(alpha = 0.3f),
size = size,
cornerRadius = CornerRadius(10f, 10f)
)
// 2. Dibujar la barra de progreso (el stat real)
drawRoundRect(
color = stat.color,
size = Size(width = progressWidth, height = size.height),
cornerRadius = CornerRadius(10f, 10f)
)
}
}
}
}
}Como puedes ver, Canvas nos da libertad absoluta para crear componentes que no existen por defecto en la librería de Compose. Este tipo de barras de progreso personalizadas serían muy complicadas de lograr con los widgets estándar. Todo el código fuente está disponible en el repositorio para que puedas experimentar con las posiciones, colores y valores:

Canvas en Android — Forma Legacy: el método onDraw()
Antes de que existiera Jetpack Compose, la manera estándar de dibujar en Android era crear una clase que extendiese de View y sobreescribir el método onDraw(Canvas). Esta clase puede estar contenida dentro de la misma actividad o en un archivo separado. A continuación llamaremos a nuestra clase personalizada MyView:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
}
}El método onDraw()
El método onDraw(Canvas canvas) recibe como parámetro una instancia de la clase Canvas, que es la que expone todas las primitivas de dibujo: líneas, círculos, rectángulos, etc. Este método es llamado automáticamente por el sistema cuando la vista necesita renderizarse o actualizarse. Para forzar un redibujado manual, puedes llamar a invalidate() desde cualquier parte de la clase, lo que programará una nueva llamada a onDraw().
Dibujando una figura:
Para dibujar cualquier figura en el enfoque Legacy, necesitas combinar dos ingredientes clave: un objeto Paint (el «pincel» que define cómo se pinta) y una llamada a una primitiva del Canvas (la función que define qué se dibuja). Veamos los principales parámetros de Paint:
- Define un objeto
Paintpara especificar el estilo visual del trazado; los métodos más comunes son:Paint.setColor: Establece el color de pintura. Acepta constantes comoColor.WHITE,Color.REDo colores en formato hexadecimal conColor.parseColor("#FF5733").Paint.setStyle: Establece el estilo de relleno. Acepta valores del enumPaint.Style:FILL: Dibuja la figura completamente rellena con el color definido.

STROKE: Dibuja únicamente el contorno de la figura, sin relleno.

FILL_AND_STROKE: Combinación de los dos anteriores: rellena la figura y dibuja también su contorno.- El objeto
Canvasdefine las formas geométricas a dibujar mediante sus primitivas:Canvas.drawColor(): Rellena todo elCanvascon un color sólido. Útil como fondo.Canvas.drawCircle(X, Y, Radius, Paint): Dibuja un círculo en la posición(X, Y)con el radioRadiusy el estilo delPaintespecificados.Canvas.drawLine(startX, startY, stopX, stopY, Paint): Dibuja una línea recta desde el punto(startX, startY)hasta(stopX, stopY).Canvas.drawRect(left, top, right, bottom, paint): Dibuja un rectángulo delimitado por las coordenadasleft,top,rightybottom.Canvas.drawPaint(Paint): Pinta todo elCanvascon el estilo delPaintespecificado, similar adrawColorpero con más opciones de estilo.
Asociando el Canvas en la actividad
Una vez que hemos definido la vista que dibujará en el Canvas, el siguiente paso consiste en asociar una instancia de la clase MyView a la actividad mediante el método setContentView():
setContentView(new MyView(this));Cuando queremos dibujar gráficos en Android, tenemos dos opciones: hacerlo dentro de la vista usando un layout XML (la forma tradicional de referenciar un layout en Android):
setContentView(R.layout.activity_main);O bien dibujar los gráficos directamente en un Canvas personalizado, como hemos hecho en este artículo:
setContentView(new MyView(this));Como puedes notar, en ambos casos empleamos el mismo método setContentView(), pero con resultados muy diferentes: en el primero inflamos un layout XML existente; en el segundo, le pasamos directamente nuestra vista personalizada que pinta en el Canvas.
¿Dibujar gráficos dentro de los layouts o directamente en el Canvas?
La recomendación general de Android es dibujar los gráficos dentro de un layout XML cuando se trata de elementos simples y sin mucho movimiento, ya que el sistema de vistas estándar está muy optimizado para ello. Sin embargo, cuando necesitas gráficos más ricos, con animaciones complejas, refrescamiento frecuente o máximo rendimiento (como en juegos 2D o visualizaciones en tiempo real), Android recomienda usar Canvas directamente, ya que ofrece un control de píxel mucho más fino y eficiente.

Dibujando un SeekBar Personalizado en Android con Canvas
El SeekBar que vamos a construir es sencillo pero muy ilustrativo: consiste en una barra horizontal (un rectángulo achatado) y una circunferencia que actúa como control deslizante. El usuario puede interactuar con él mediante un gesto de toque para desplazar la bola de derecha a izquierda y viceversa, actualizando el valor en tiempo real.
Definiendo las bases: la estructura de la actividad
Primero debemos definir la estructura de nuestra actividad, que será la que dé soporte al SeekBar dibujado mediante Canvas en una clase separada. En el layout de la actividad hacemos realmente poco; simplemente definimos un contenedor vacío que servirá como raíz:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.presentacion.desarrollolibre.audioplayer.MainActivity">
</LinearLayout>Desde la actividad, creamos programáticamente un FrameLayout con unas dimensiones fijas de 300 × 300 píxeles y le asignamos nuestra vista personalizada usando el siguiente código Java:
SeekBarView seekBarView = new SeekBarView(MainActivity.this);
FrameLayout.LayoutParams myFrameLayoutParams = new FrameLayout.LayoutParams(300,300);
seekBarView.setLayoutParams(myFrameLayoutParams);
setContentView(seekBarView);La clase SeekBarView: dibujando el SeekBar con Canvas
La clase SeekBarView extiende de FrameLayout y sobreescribe ciertos métodos de esta clase. En el constructor, creamos un objeto de tipo Paint que nos permite definir el estilo visual de las formas geométricas (nuestro rectángulo y círculo) antes de dibujarlas en el Canvas. Además, definimos dos variables con un tamaño fijo que controlan las dimensiones del elemento deslizante:
thumbWidth = dp(24);
thumbHeight = dp(24);
Estas variables definen la posición y el tamaño del control circular que dibujamos dentro del método onDraw() de la clase SeekBarView. Dicho método sobreescrito se encarga de crear el lienzo y dibujar las dos figuras geométricas: la barra (rectángulo) y el control deslizante (círculo):
canvas.drawRect(thumbWidth / 2, getMeasuredHeight() / 2 - dp(1), thumbWidth / 2 + thumbX, getMeasuredHeight() / 2 + dp(1), outerPaint1);
El método getMeasuredHeight() retorna el alto del contenedor que definimos en la actividad; en este experimento, ese valor es de 300 píxeles.
El método getMeasuredWidth() retorna el ancho del contenedor; también 300 píxeles en este caso. Estas medidas son esenciales para calcular las posiciones relativas de las figuras dentro del Canvas.
Además de la barra dibujada con drawRect, dibujamos el control circular con la primitiva drawCircle. Nótese que el radio del círculo cambia ligeramente según si el usuario está presionando o no (pressed ? 8 : 6), lo que da un pequeño feedback visual táctil:
canvas.drawCircle(thumbX + thumbWidth / 2, y + thumbHeight / 2, dp(pressed ? 8 : 6), outerPaint1);
Por último, se implementa el método onTouch, que captura los eventos de toque del usuario sobre el Canvas para actualizar la posición del control deslizante en tiempo real. El método onTouch es especialmente interesante porque realiza los cálculos necesarios para reubicar la bola según el movimiento del dedo, actualizando la variable thumbX con la posición horizontal actual del toque. Tras cada actualización, llama automáticamente a invalidate(), lo que fuerza un redibujado completo de la vista con las nuevas coordenadas:
boolean onTouch(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
getParent().requestDisallowInterceptTouchEvent(true);
int additionWidth = (getMeasuredHeight() - thumbWidth) / 2;
if (thumbX - additionWidth <= ev.getX() && ev.getX() <= thumbX + thumbWidth + additionWidth && ev.getY() >= 0 && ev.getY() <= getMeasuredHeight()) {
pressed = true;
thumbDX = (int) (ev.getX() - thumbX);
invalidate();
return true;
}
} else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
if (pressed) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
// onSeekBarDrag((float) thumbX / (float) (getMeasuredWidth() - thumbWidth));
}
pressed = false;
invalidate();
return true;
}
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (pressed) {
thumbX = (int) (ev.getX() - thumbDX);
if (thumbX < 0) {
thumbX = 0;
} else if (thumbX > getMeasuredWidth() - thumbWidth) {
thumbX = getMeasuredWidth() - thumbWidth;
}
invalidate();
return true;
}
}
return false;
}
En el método onTouch manejamos tres eventos clave de MotionEvent: la constante ACTION_DOWN, que se dispara al momento de iniciar el toque sobre el Canvas; la constante ACTION_MOVE, que se ejecuta de forma continua mientras el dedo se arrastra; y la constante ACTION_UP, que se activa al levantar el dedo y marca el final del gesto.
A raíz de estos estados, se actualiza la variable thumbX, que es la que «mueve» o permite redibujar la bola mediante el método onDraw(). Este ciclo continuo de onTouch → invalidate() → onDraw() es el patrón fundamental de cualquier animación o interacción basada en Canvas en el sistema de vistas de Android.
Siguiente paso: aprende a generar códigos QR en Android con Compose.