Crear una Tarjeta o Card como contenedor en Android Studio | Jetpack Compose
Índice de contenido
- Introducción a las Cards en Jetpack Compose
- Cartas básicas
- Mejorando el Diseño con Modificadores
- Creando una “Card Bonita”
- El poder del Layout: Box vs. Column/Row
- Resto del diseño
- Conclusión y Práctica
- CardViews con XML y Kotlin (Enfoque Legacy)
- ¿Para qué sirve un CardView?
- Creando nuestro CardView con ConstraintLayout
- El Layout Completo
- (Opcional) Tipografías en Android
Veamos como trabajar con las Card que es el enfoque moderno o los CardViews que es el enfoque basado en Views con XML.
Quedamos en que conocemos como emplear los Dialogs en Android con Jetpack Compose.
Introducción a las Cards en Jetpack Compose
Las Cards (cartas o tarjetas) son otro elemento de interfaz gráfica que no puede faltar. Como siempre, si realizas una búsqueda rápida como "Card Android Studio Compose", verás que la documentación oficial ofrece ejemplos claros sobre cómo cambiar parámetros como la elevación (para las sombras) y los colores.
Por convención, una carta es simplemente un contenedor, usualmente con bordes redondeados y una elevación que le otorga profundidad. Es ideal para presentar datos de manera organizada. De momento, no estamos profundizando demasiado en diseño, pero pronto practicaremos con una aplicación real.
Cartas basicas
El uso básico en Compose es sumamente sencillo. Simplemente llamas a la función Card y dentro colocas el contenido, como un texto:
@Composable
fun CardMinimalExample() {
Card() {
Text(text = "Hello, world!")
}
}
@Composable
fun FilledCardExample() {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
modifier = Modifier
.size(width = 240.dp, height = 100.dp)
) {
Text(
text = "Filled",
modifier = Modifier
.padding(16.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun CardPreview() {
MyProyectAndroidTheme {
// FilledCardExample()
CardBonito()
}
}Mejorando el Diseño con Modificadores
Un ejemplo básico puede ser aburrido, así que vamos a mejorarlo. Usando modificadores (Modifiers), podemos definir el tamaño máximo, mínimo y el padding:
.size(width = 240.dp, height = 100.dp)Recuerda que en Android trabajamos con densidades de píxeles (dp) y no con píxeles directos.
Creando una “Card Bonita”
Para este ejercicio, quise recrear un diseño similar al antiguo enfoque de XML (CardView) que esta abajo en este Post. Pero usando la potencia de Compose. Le pedí asistencia a Gemini para generar una interfaz más visual y este fue el resultado:

Utilizamos una función Composable llamada CardBonito que recibe parámetros y modificadores:
@Composable
fun CardBonito(
count: String = "123",
date: String = "20/01/2026",
title: String = "Título de ejemplo",
direction: String = "Av. Siempre Viva 123",
mount: String = "$1,500.00"
) {
// El Box principal actúa como el RelativeLayout contenedor
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 1. LA CARD (Contenedor principal de datos)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp), // Espacio para el "Mount" que sobresale arriba
shape = RoundedCornerShape(5.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF6200EE)) // Color de ejemplo
) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
) {
}
}
// 2. EL MONTO (Texto centrado arriba que sobresale)
Surface(
modifier = Modifier.align(Alignment.TopCenter),
color = Color(0xFF3700B3), // Color de fondo del monto
shape = RoundedCornerShape(8.dp),
shadowElevation = 4.dp
) {
}
// 3. LOS BOTONES FLOTANTES (FABs en horizontal)
Row(
modifier = Modifier
.align(Alignment.BottomCenter) // Los posicionamos abajo al centro
.offset(y = 20.dp), // Los movemos hacia afuera de la card para el efecto visual
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
}
}
}El poder del Layout: Box vs. Column/Row
Aquí viene el truco del ejercicio: ¿cómo logramos colocar elementos "flotando" o superpuestos? Para esto usamos el Box, que es el equivalente al RelativeLayout. A diferencia de Column (vertical) o Row (horizontal), donde cada elemento ocupa su propio espacio y no se pueden pisar, el Box es el equivalente al RelativeLayout. Funciona como una pila de libros:
- El primer elemento (la Card) sirve como lienzo o base.
- Los siguientes elementos se apilan encima.
Si quitamos el Box y los alineamientos, todos los elementos se amontonarían uno sobre otro en la esquina superior izquierda. Gracias a esta herramienta, podemos superponer iconos o etiquetas sobre la tarjeta sin alterar la estructura interna de la misma.
Resto del diseño
El resto del diseño, es mas de lo mismo, textos, botones e iconos con estilo:
@Composable
fun CardBonito(
count: String = "123",
date: String = "20/01/2026",
title: String = "Título de ejemplo",
direction: String = "Av. Siempre Viva 123",
mount: String = "$1,500.00"
) {
// El Box principal actúa como el RelativeLayout contenedor
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 1. LA CARD (Contenedor principal de datos)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp), // Espacio para el "Mount" que sobresale arriba
shape = RoundedCornerShape(5.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF6200EE)) // Color de ejemplo
) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
) {
// Fila Superior (Count y Date) - Equivalente al RelativeLayout interno
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = count, color = Color.White, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodySmall)
Text(text = date, color = Color.White, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(16.dp))
// Título
Text(
text = title,
color = Color.White,
fontSize = 20.sp,
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Medium
)
// Dirección
Text(
text = direction,
color = Color.White,
style = MaterialTheme.typography.bodySmall
)
// Espacio extra al final (el View vacío del XML)
Spacer(modifier = Modifier.height(24.dp))
}
}
// 2. EL MONTO (Texto centrado arriba que sobresale)
Surface(
modifier = Modifier.align(Alignment.TopCenter),
color = Color(0xFF3700B3), // Color de fondo del monto
shape = RoundedCornerShape(8.dp),
shadowElevation = 4.dp
) {
Text(
text = mount,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = Color.White,
style = MaterialTheme.typography.headlineMedium,
fontFamily = FontFamily.SansSerif
)
}
// 3. LOS BOTONES FLOTANTES (FABs en horizontal)
Row(
modifier = Modifier
.align(Alignment.BottomCenter) // Los posicionamos abajo al centro
.offset(y = 20.dp), // Los movemos hacia afuera de la card para el efecto visual
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
SmallFloatingActionButton(
onClick = { /* Info */ },
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(Icons.Default.Info, contentDescription = "Info")
}
SmallFloatingActionButton(
onClick = { /* Edit */ },
containerColor = MaterialTheme.colorScheme.secondary
) {
Icon(Icons.Default.Edit, contentDescription = "Edit")
}
}
}
}- En lugar de usar solo Column, empleamos un Row con Arrangement.SpaceBetween. Esto aleja los elementos hacia los extremos (como el monto y la fecha), de forma muy similar a como se hace en Flutter
- Utilizamos Spacer con medidas en dp para separar elementos, y para los textos usamos sp (píxeles escalables), que es la unidad estándar para tipografías.
- En el código verás que utilicé un Surface para mostrar el monto. Técnicamente, el Surface es un elemento de menor nivel; de hecho, la Card implementa un Surface por debajo para añadir funcionalidades como sombras o eventos de clic (onClick). Para efectos prácticos, ambos son similares, pero la Card tiene características adicionales listas para usar.
- Mediante el modificador align, decidimos dónde colocar cada pieza (arriba al centro, abajo a la derecha, etc.).
Conclusión y Práctica
Como puedes ver en el emulador, el resultado es una interfaz declarativa muy potente. Te invito a que tomes el código del repositorio, cambies los valores del Arrangement (prueba con .Center o .SpaceAround) y varíes los alineamientos del Box. La mejor forma de entender esta interfaz es rompiéndola y volviéndola a armar.
CardViews con XML y Kotlin (Enfoque Legacy)
Veremos cómo crear CardViews personalizados con las herramientas modernas de desarrollo de Android. Nos enfocaremos en la vista utilizando AndroidX, Material Components y ConstraintLayout, sin adentrarnos en la lógica de la Actividad. La meta es diseñar un ítem de lista complejo con varias opciones, información y una sección destacada.
Los CardView son excelentes contenedores para mostrar datos de manera resumida y detallada; son muy empleados para crear los items de los listados mediante los RecyclerView.
Como puedes ver en la imagen promocional, existen ciertos elementos alineados de una manera que a primera vista puede no resultar tan fácil de deducir su organización, pero que en realidad no es compleja su construcción.
Resumiendo un poco e indicando cada uno de los elementos en la siguiente imagen de manera descendente, nuestra vista contará con 3 secciones principales:
- Sección destacada.
- El
CardViewcomo tal. - La sección de opciones.

¿Para qué sirve un CardView?
Los CardView son un componente de Material Design que cuentan con un diseño similar a un widget de manera nativa, con el típico sombreado y bordes redondeados. Son muy empleados en conjunto con los RecyclerView.
Contiene una serie de propiedades únicas como el redondeo de bordes (app:cardCornerRadius), elevación (app:cardElevation), etc. Puedes consultar la documentación oficial para más información: Crear vistas con tarjetas.
Creando nuestro CardView con ConstraintLayout
Para crear nuestro diseño, usaremos un ConstraintLayout como contenedor principal. Este layout nos permite crear jerarquías de vistas planas y complejas con un rendimiento superior, siendo la opción recomendada por Google sobre RelativeLayout o LinearLayout anidados.
Dentro del ConstraintLayout, definiremos 3 elementos principales anclados entre sí:
- El
CardViewdeandroidx.cardview.widget.CardView. - El
TextViewpara la sección destacada, posicionado para solaparse con el borde superior delCardView. - Un
LinearLayoutcon losFloatingActionButtondecom.google.android.material.floatingactionbutton.FloatingActionButtonpara las opciones.
El truco para el solapamiento es jugar con los márgenes y la elevación. El CardView tiene un margen superior para dejar espacio al TextView destacado, y este a su vez tiene una elevación para asegurarse de que se dibuje por encima del card. Los botones (FABs) se anclan a la parte inferior del CardView con un margen negativo para que se superpongan ligeramente.
El Layout Completo
A continuación se muestra el código XML completo del layout. Nota cómo los atributos app:layout_constraint... definen las relaciones entre los elementos para lograr el diseño deseado.
"1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingBottom="16dp">
<androidx.cardview.widget.CardView
android:id="@+id/cvContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="45dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:text="title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FFFFFF"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:gravity="right"
android:text="title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FFFFFF"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginEnd="5dp"
android:fontFamily="sans-serif-condensed"
android:text="title"
android:textColor="#FFFFFF"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCount" />
<TextView
android:id="@+id/tvDirection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="sans-serif-light"
android:text="title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FFFFFF"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp"
app:layout_constraintTop_toBottomOf="@id/tvDirection"
app:layout_constraintStart_toStartOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/llFabContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="-20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cvContainer">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_action_info_outline"
app:fabSize="mini" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_action_edit"
app:fabSize="mini" />
LinearLayout>
<TextView
android:id="@+id/tvMount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/layout_bg_corner_mount_1"
android:fontFamily="sans-serif-thin"
android:padding="10dp"
android:text="title"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="#FFFFFF"
android:textSize="25sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="25dp"
android:elevation="8dp"/>
androidx.constraintlayout.widget.ConstraintLayout>Los valores para dimensiones (dimens.xml), colores (colors.xml) y formas (drawable) siguen siendo conceptos fundamentales. Los que se usaron en el diseño original son compatibles con este nuevo layout.
dimens.xml
<dimen name="space_component2x">10dpdimen>
<dimen name="space_component3x">15dpdimen>
<dimen name="fab_margin">10dpdimen>
<dimen name="space_big">20dpdimen>
<dimen name="card_detail_mount_margin">45dpdimen>drawable/layout_bg_corner_mount_1.xml
"1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorFive"/>
<stroke android:width="3dp" android:color="@color/colorFive" />
<corners android:radius="30dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
shape>colors.xml
<color name="colorFive">#F7C51Ecolor>(Opcional) Tipografías en Android
Para darle un poco de estilo, se puede cambiar la tipografía. Los valores de android:fontFamily siguen siendo válidos:
android:fontFamily="sans-serif" // roboto regular
android:fontFamily="sans-serif-light" // roboto light
android:fontFamily="sans-serif-condensed" // roboto condensed
android:fontFamily="sans-serif-black" // roboto black
android:fontFamily="sans-serif-thin" // roboto thin (API 16+)
android:fontFamily="sans-serif-medium" // roboto medium (API 21+)Representados en la siguiente imagen:

Hoy en día, la forma recomendada de usar fuentes personalizadas es a través de la carpeta de recursos res/font. Esto permite empaquetar archivos de fuentes (.ttf o .otf) directamente en tu APK y acceder a ellos de forma segura con android:fontFamily="@font/mi_fuente".
El siguiente paso, conoce como como usar el RecyclerView en Jetpack Compose - LazyColumn y LazyRow
Acepto recibir anuncios de interes sobre este Blog.
Una carta es un contenedor, usualmente con bordes redondeados y una elevación que le otorga profundidad. Es ideal para presentar datos de manera organizada.