Crear una Tarjeta o Card como contenedor en Android Studio | Jetpack Compose

Video thumbnail

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:

cardview estructura en android

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 CardView como tal.
  • La sección de opciones.
cardview estructura en android

¿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 CardView de androidx.cardview.widget.CardView.
  • El TextView para la sección destacada, posicionado para solaparse con el borde superior del CardView.
  • Un LinearLayout con los FloatingActionButton de com.google.android.material.floatingactionbutton.FloatingActionButton para 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:

Tipografía en android

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.

| 👤 Andrés Cruz

🇺🇸 In english