La barra de búsqueda en Android
Al terminar: Poner la barra de búsqueda de Material 3 sobre el estado compartido.
Al terminar esta lección, la app de Android busca: tocas una barra de Material 3, se expande a pantalla completa con los resultados dentro, y cada tecla llega al mismo PokemonSearchViewModel de la lección anterior. La barra la pone un componente; lo que se ve en cada momento lo decide el estado compartido.
Es la primera de dos lecciones. En la siguiente, La búsqueda en iOS con .searchable, el campo lo coloca el sistema (bajo el título hasta iOS 18; flotando abajo en iOS 26) y trae su propio botón de borrar. Debajo de las dos está el mismo PokemonSearchViewModel, el mismo debounce y el mismo índice. Es la prueba de que compartir el estado y no compartir el aspecto funciona: la misma lógica, dos interfaces que un usuario reconoce como suyas.
No se instala nada: Material 3 llegó con El módulo compartido y la prueba de humo, Coil con La lista en Android y los iconos con Navegación en Android con Navigation 3. Se escribe en Android Studio, con la vista Project (carpetas androidApp y shared).
Lo que cada plataforma da hecho, y lo que no
| Android | iOS | |
|---|---|---|
| Qué es | un componente (SearchBar) que se coloca | un modificador (.searchable) |
| Dónde vive | ocupa la pantalla al expandirse, resultados dentro | campo que coloca el sistema: bajo el título hasta iOS 18; flotando abajo desde iOS 26 |
| Título grande | hay que decidir qué hacer con él | lo gestiona el sistema |
| Estado vacío | se compone a mano | ContentUnavailableView, con el caso «sin resultados» hecho |
| Limpiar | icono que hay que poner | lo pone el sistema |
Y el mueble de Android no viene vacío: SearchBar te da hechas tres cosas que de otro modo programarías tú.
- La animación de expansión. La barra crece hasta ocupar la pantalla con una transición de Material ya afinada.
- El foco y el teclado. Pide el foco al expandirse y lo suelta al plegarse.
- El gesto atrás. Con la barra expandida, atrás la pliega en vez de cerrar la pantalla; hecho a mano, es justo lo que se olvida y solo se descubre probando.
El concepto · Una barra que pregunta y avisa
SearchBar es el componente de Material 3 para buscar. Tiene dos partes: el campo (la píldora con la lupa donde se escribe, que se declara en inputField) y el hueco, la lambda final del componente, que es lo que se ve cuando la barra se expande. En la Pokédex, el hueco llevará los resultados. Las dos son lambdas que pintan algo, como el topBar = { … } de Scaffold que ya usaste: entre las llaves va lo que se verá en ese sitio.
Lo que más sorprende es que SearchBar no recuerda si está abierta. Tú le dices si lo está con expanded (verdadero o falso) y ella te avisa con onExpandedChange cuando el usuario toca el campo o pulsa atrás. Hace falta, por tanto, un sitio donde guardar esa bandera, y ese sitio es quien usa la barra.
Un ejemplo solo para leer (no lo escribas en el proyecto), con datos ajenos a la Pokédex: una barra que busca entre cuatro recetas. Los import los propone Android Studio con Option+Enter sobre cada nombre en rojo.
private val recetas = listOf("Tortilla", "Gazpacho", "Paella", "Tarta de queso")
@OptIn(ExperimentalMaterial3Api::class) // SearchBar aún es «experimental»
@Composable
fun BuscadorDeRecetas() {
var texto by rememberSaveable { mutableStateOf("") } // lo escrito
var abierta by rememberSaveable { mutableStateOf(false) } // ¿expandida?
SearchBar(
inputField = { // el campo: qué se ve arriba
SearchBarDefaults.InputField(
query = texto, // el texto baja…
onQueryChange = { texto = it }, // …y cada tecla sube
onSearch = { abierta = false }, // la tecla «buscar» del teclado
expanded = abierta,
onExpandedChange = { abierta = it }, // tocar el campo pide expandir
placeholder = { Text("Buscar receta") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
)
},
expanded = abierta, // el componente obedece a esta bandera
onExpandedChange = { abierta = it }, // atrás pide plegar
) {
// El hueco final: lo que se ve con la barra expandida.
Column {
recetas.filter { it.contains(texto, ignoreCase = true) }
.forEach { Text(it, Modifier.fillMaxWidth().padding(16.dp)) }
}
}
}Lo que hace es lo mismo que hará dentro de un rato la barra de la Pokédex:
- una píldora con la lupa y «Buscar receta»;
- al tocarla, ocupa la pantalla y sale el teclado;
- al escribir «ta», quedan «Tortilla» y «Tarta de queso»;
- el primer atrás esconde el teclado y el segundo pliega la barra.
Dos ideas, en dos frases. El componente no guarda si está abierto: lo pregunta (expanded) y avisa (onExpandedChange). Y como la barra está hecha de dos piezas y las dos necesitan saberlo, hay dos sitios con expanded: el campo lo lee para saber en qué estado está cuando lo tocas o recibe el foco, y la barra lo lee para decidir si ocupa la pantalla entera o es una píldora. Los dos leen la misma bandera, la abierta de arriba, y por eso nunca se contradicen.
Una pista sobre el nombre SearchBarDefaults.InputField: en Material, un …Defaults («valores por defecto») es la caja de piezas ya preparadas que acompaña a un componente. SearchBarDefaults guarda las de SearchBar, aquí el campo de texto que inputField espera; lo mismo pasa con TopAppBarDefaults, que viste en Refresco, barra y accesibilidad en Android.
Los ficheros de hoy
Paso 1 · Android: la barra es un componente que envuelve
El archivo. PokemonSearchBar.kt es la barra de búsqueda de la Pokédex: un campo de texto que se expande y, dentro, la lista de resultados. Vive en un paquete nuevo, search, dentro de androidApp. Créalo: clic derecho sobre la carpeta androidApp → New → File y la ruta desde esa carpeta:
src/main/kotlin/com/sebastiannarvaez/pokedex/android/search/PokemonSearchBar.ktPokemonSearchBar tampoco guarda nada: recibe estado y expandida y avisa con lambdas (alEscribir, alLimpiar…). Empieza por la cabecera: los import, la documentación y la firma. Los parámetros son exactamente esa lista de «estado hacia abajo, eventos hacia arriba»:
package com.sebastiannarvaez.pokedex.android.search
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.sebastiannarvaez.pokedex.domain.PokemonRef
import com.sebastiannarvaez.pokedex.feature.search.SearchUiState
/**
* La barra de busqueda de Material 3.
*
* Se expande a pantalla completa con sus resultados dentro, y el propio
* componente gestiona la animacion, el foco y el gesto de volver. En iOS esto
* lo hace el sistema con un modificador; aqui es un componente que envuelve.
*
* Los resultados se pintan como **filas**, no como tarjetas: una lista larga se
* recorre mejor en vertical, y ademas no tenemos los tipos de cada resultado.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PokemonSearchBar(
estado: SearchUiState,
expandida: Boolean,
alCambiarExpansion: (Boolean) -> Unit,
alEscribir: (String) -> Unit,
alLimpiar: () -> Unit,
alElegir: (PokemonRef) -> Unit,
modifier: Modifier = Modifier,
) {Sigue el campo de entrada. Cuenta lo que hay ahí y que en iOS no vas a escribir: el icono de la lupa, el icono de borrar con su contentDescription (el texto que lee TalkBack, el lector de pantalla de Android), el texto de sugerencia (placeholder) y las dos rutas del estado de expansión. Cinco decisiones. onSearch es lo que ocurre cuando el usuario pulsa la tecla de buscar del teclado: aquí, alCambiarExpansion(true), es decir, dejar la barra expandida. Es distinto del ejemplo de las recetas, donde esa tecla la plegaba (abierta = false) para que se viera la pantalla de debajo. En la Pokédex los resultados viven dentro de la barra, y se ven mientras escribes: plegarla al pulsar buscar escondería justo lo que el usuario acaba de pedir. El SearchBar aún tiene un hueco, su lambda final, que es donde van los resultados:
SearchBar(
modifier = modifier,
inputField = {
SearchBarDefaults.InputField(
query = estado.consulta,
onQueryChange = alEscribir,
onSearch = { alCambiarExpansion(true) },
expanded = expandida,
onExpandedChange = alCambiarExpansion,
placeholder = { Text("Buscar Pokémon") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (estado.consulta.isNotEmpty()) {
IconButton(onClick = alLimpiar) {
Icon(Icons.Default.Close, contentDescription = "Borrar la búsqueda")
}
}
},
)
},
expanded = expandida,
onExpandedChange = alCambiarExpansion,
) {Dentro del hueco, el contenido, que es el SearchUiState traducido a pantalla. Son cinco ramas, evaluadas en orden con when: reposo, cargando, error, sin resultados y, si ninguna aplica, la lista. Dos detalles. estado.error!! es la aserción de no nulo: le dice al compilador «aquí no es null» y falla si lo es (Nulabilidad). Es seguro porque justo antes se comprobó estado.error != null; pero entonces, ¿para qué hace falta el !!? Porque error es una propiedad de SearchUiState, una clase que vive en otro módulo (shared), y el compilador no da por buena una comprobación sobre una propiedad de otro módulo: allí podría cambiar entre la comprobación y el uso, así que sigue tratándola como posiblemente nula. El !! es tu palabra de que no lo es. Y key = { it.id } es lo que le permite a LazyColumn reconocer cada fila si la lista cambia (Listas con LazyColumn). La línea Modifier.imePadding() de la lista se explica justo debajo del bloque:
when {
estado.enReposo -> Aviso("Escribe un nombre para empezar")
estado.buscando && estado.resultados.isEmpty() ->
Box(Modifier.fillMaxWidth().padding(32.dp), Alignment.Center) { CircularProgressIndicator() }
estado.error != null -> Aviso(estado.error!!.detalle)
estado.sinResultados -> Aviso("Ningún Pokémon se llama así")
// imePadding: la lista termina encima del teclado, y los ultimos
// resultados se alcanzan desplazando sin tener que cerrarlo.
else -> LazyColumn(Modifier.imePadding()) {
items(estado.resultados, key = { it.id }) { ref ->
FilaDeResultado(ref, alPulsar = { alElegir(ref) })
}
}
}
}
}El orden de las ramas no es negociable. enReposo va primero porque con el campo vacío no se ha buscado nada, y decir «ningún Pokémon se llama así» antes de que el usuario escriba es acusarle de algo que no ha hecho. Por eso enReposo y sinResultados viven en el estado compartido: son la misma decisión en las dos apps.
El teclado también ocupa sitio: imePadding()
Esos eran dos insets: la barra de estado y la barra de gestos. El teclado en pantalla es el tercero, y el que más sitio ocupa. Android lo llama IME, de Input Method Editor («editor de método de entrada»): la pieza del sistema que recibe lo que tecleas y se lo entrega a la app. Cuando sale, sube desde abajo y se pone encima de tu pantalla; no la empuja ni la encoge.
Y ahí está el problema: la lista de resultados no se entera. Cree que tiene toda la pantalla, y diez filas caben, así que ni siquiera se desplaza. Pero las dos últimas están físicamente debajo del teclado. Es lo que pasa con «char» si no haces nada: Chimchar queda cortado y Pecharunt, tapado. Medido en el emulador (un Pixel 8 Pro), el teclado empieza a 1985 píxeles del borde de arriba y Pecharunt estaba a 2133: 148 píxeles por debajo, sin forma de llegar a él sin cerrar el teclado.
Modifier.imePadding() es el modificador que pide ese inset. Añade por abajo un relleno tan alto como el teclado mientras está abierto, y cero cuando no. La LazyColumn pasa a medir solo lo que queda encima del teclado, deja de caber entera y se puede desplazar hasta el final: Chimchar y Pecharunt quedan por encima del teclado. Con el teclado cerrado, la lista llega abajo como antes.
Si quieres verlo con tus ojos, quita Modifier.imePadding() (deja LazyColumn { a secas), ejecuta, abre la barra, escribe «char» e intenta desplazar la lista con el teclado abierto: no se mueve. Devuelve el modificador a su sitio antes de seguir.
Quedan las dos piezas pequeñas que usa: la fila de cada resultado (la ilustración sale de artworkUrl, y se carga con Coil) y el aviso de texto. Un detalle de la fila: ref.id.toString().padStart(4, '0') convierte el número en texto y lo rellena con ceros por la izquierda hasta 4 caracteres (el 25 se escribe «0025»).
@Composable
private fun FilaDeResultado(ref: PokemonRef, alPulsar: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = alPulsar)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {
AsyncImage(
model = ref.artworkUrl,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp)),
)
Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
ref.name.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
)
Text(
"N.º ${ref.id.toString().padStart(4, '0')}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@Composable
private fun Aviso(texto: String) {
Text(
text = texto,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(32.dp),
)
}Paso 2 · El título grande estorba, y hay que decidirlo
El archivo. PokemonListScreen.kt ya existe (ábrelo con Navigate → File…, Cmd+Shift+O). Hoy se le hacen cinco cambios, de arriba abajo, y uno más en PokedexApp.kt al final. Si en cualquiera de ellos algo sale en rojo, es un nombre que falta por importar: Option+Enter sobre él y elige el import. Un recordatorio de lo que ya hay en el fichero: LargeTopAppBar es la barra de Material con el título grande que se encoge al hacer scroll, y comportamiento (el scrollBehavior) es el objeto que le dice cómo encogerse; lo copiaste con el fichero en La lista en Android y lo explica Refresco, barra y accesibilidad en Android.
1 · Los import nuevos, junto a los que ya hay:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sebastiannarvaez.pokedex.android.search.PokemonSearchBar
import com.sebastiannarvaez.pokedex.feature.search.PokemonSearchViewModel
import com.sebastiannarvaez.pokedex.feature.search.SearchUiStateY uno más, el de AnimatedVisibility, que Android Studio ordena el primero de los androidx.compose (va antes que los de androidx.compose.foundation; el orden no afecta a que compile): import androidx.compose.animation.AnimatedVisibility. Los otros nombres que usa el fichero (Column, Alignment, padding, dp, FontWeight) ya estaban importados. Si algo sale en rojo, Option+Enter.
2 · La pantalla recibe el buscador. Hasta hoy, al pulsar un Pokémon se avisaba con un Pokemon entero. De la búsqueda no sale un Pokemon, sale un PokemonRef, y lo único que las dos cosas tienen en común es el identificador, que es justo lo que la navegación necesita: alPulsar pasa a recibir un Int. Antes:
fun PokemonListScreen(
alPulsar: (Pokemon) -> Unit,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel = koinViewModel(),
) {
val pokemon = viewModel.pokemon.collectAsLazyPagingItems()
PokemonListContent(pokemon = pokemon, alPulsar = alPulsar, modifier = modifier)
}Después (cambia la función entera). Tres piezas que ya conoces trabajan juntas:
@Composable
fun PokemonListScreen(
alPulsar: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel = koinViewModel(),
buscador: PokemonSearchViewModel = koinViewModel(),
) {
// collectAsLazyPagingItems es el puente que Android ya trae hecho. En iOS
// hubo que escribirlo a mano: es la misma pieza, aqui regalada.
val pokemon = viewModel.pokemon.collectAsLazyPagingItems()
val busqueda by buscador.uiState.collectAsStateWithLifecycle()
var expandida by rememberSaveable { mutableStateOf(false) }
PokemonListContent(
pokemon = pokemon,
busqueda = busqueda,
expandida = expandida,
alCambiarExpansion = { expandida = it },
alEscribir = buscador::escribir,
alLimpiar = buscador::limpiar,
alPulsar = alPulsar,
modifier = modifier,
)
}La bandera de expansión se guarda con rememberSaveable, no con remember: con remember, girar el móvil con la búsqueda abierta la cerraría de golpe. El texto escrito sobreviviría —vive en el ViewModel— pero la pantalla volvería a la rejilla, que es peor que perder el texto: parece que la app se ha ido sola. Fíjate además en dónde vive cada cosa: el texto y los resultados, en el estado compartido; expandida, aquí, en la pantalla de Android. iOS no tiene una barra que se expanda, así que esa bandera no puede estar en el ViewModel de las dos.
3 · PokemonListContent recibe lo que la barra necesita. Sustituye su cabecera (de private fun PokemonListContent( a su ) {) por esta:
private fun PokemonListContent(
pokemon: LazyPagingItems<Pokemon>,
busqueda: SearchUiState,
expandida: Boolean,
alCambiarExpansion: (Boolean) -> Unit,
alEscribir: (String) -> Unit,
alLimpiar: () -> Unit,
alPulsar: (Int) -> Unit,
modifier: Modifier = Modifier,
) {4 · La barra superior deja de ser el LargeTopAppBar a secas. Dentro del Scaffold, el parámetro topBar = { … } pasa a ser una columna con el título (que desaparece al expandir la búsqueda) y la barra:
topBar = {
Column {
// El titulo grande desaparece al abrir la busqueda: con la
// barra expandida ocupando la pantalla, dejarlo arriba deja un
// hueco muerto y compite por la atencion.
AnimatedVisibility(visible = !expandida) {
LargeTopAppBar(
title = { Text("Pokédex", fontWeight = FontWeight.Bold) },
scrollBehavior = comportamiento,
)
}
PokemonSearchBar(
estado = busqueda,
expandida = expandida,
alCambiarExpansion = alCambiarExpansion,
alEscribir = alEscribir,
alLimpiar = alLimpiar,
alElegir = { ref ->
alCambiarExpansion(false)
alPulsar(ref.id)
},
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp),
)
}
},Esa decisión es la que iOS no te deja tomar: en Android el título grande y la barra de búsqueda son dos componentes independientes, así que alguien tiene que decir qué pasa cuando la búsqueda se expande. Aquí el título se va, y al elegir un resultado se cierra la barra antes de navegar. (Modifier.align(Alignment.CenterHorizontally) es el modificador que, dentro de una Column, coloca a un hijo en el eje horizontal: aquí, centra la barra.)
5 · La rejilla también pasa el identificador. Dos líneas dentro de Rejilla: su firma y la que pinta cada tarjeta. Antes:
private fun Rejilla(pokemon: LazyPagingItems<Pokemon>, alPulsar: (Pokemon) -> Unit) {
// …
pokemon[indice]?.let { PokemonCard(it, alPulsar = { alPulsar(it) }) }Después (el resto de la función no cambia). Fíjate en que el Pokémon pasa de llamarse it a p. No hace falta para que compile (con it también funcionaría), pero dentro de la lambda alPulsar = { … } un it.id suelto se lee mal: ¿de quién es? Con p, p.id dice que es el Pokémon de esa tarjeta:
private fun Rejilla(pokemon: LazyPagingItems<Pokemon>, alPulsar: (Int) -> Unit) {
// …
pokemon[indice]?.let { p -> PokemonCard(p, alPulsar = { alPulsar(p.id) }) }6 · Y el cambio fuera de este fichero. Es PokedexApp.kt, en el paquete navigation: el fichero de la pila de pantallas que creaste en La pila de pantallas en Android. Allí entry<ListaKey> { … } dice qué pintar cuando la lista es la pantalla de arriba, y antes sacaba el identificador de un Pokemon:
alPulsar = { pokemon -> pila.add(DetalleKey(pokemon.id)) },Dentro de esa entry, sustituye el argumento alPulsar por este, que ya recibe el identificador y no necesita sacarlo de ningún Pokemon:
alPulsar = { id -> pila.add(DetalleKey(id)) },Compila Android: ./gradlew :androidApp:assembleDebug debe terminar en BUILD SUCCESSFUL. Ejecuta la app (Run) en el emulador y prueba la barra.
Punto de control
- Tocas la barra: se expande a pantalla completa, el título grande desaparece con animación, sale el teclado y se lee «Escribe un nombre para empezar».
- Escribes «char» y salen diez resultados, en este orden: Charmander, Charmeleon, Charizard, Charjabug, Charcadet, Charizard-mega-x, Charizard-mega-y, Charizard-gmax, Chimchar y Pecharunt (las formas especiales, con su 404 al tocarlas, como dice el aviso de arriba).
- Con el teclado abierto, desplazas la lista de «char» hasta Chimchar y Pecharunt, que quedan por encima del teclado. Sin
imePadding()quedaban debajo y la lista no se movía. - Escribes «zzzzchar» y se lee «Ningún Pokémon se llama así».
- Giras el móvil con la búsqueda abierta y sigue abierta, con el texto puesto.
- Atrás, una vez, esconde el teclado; otra vez, pliega la barra.
- Eliges un resultado, por ejemplo Charizard, y se abre su ficha. Al volver, la barra está plegada, pero conserva «char» y la ✕ para borrarlo.
./gradlew :androidApp:assembleDebugtermina enBUILD SUCCESSFUL, y los tests siguen en 41 en la JVM, 40 en Android host y 42 en iOS (./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test, sin contar los de la carpetaejercicios). Esta lección no añade ninguno.
Cada plataforma regala cosas distintas, y copiar el patrón de la otra te deja pagando a mano lo que la tuya ya te daba. Usa cada una como está pensada y las dos apps saldrán mejor que si las igualas. Falta la mitad de iOS: La búsqueda en iOS con .searchable.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.¿Cuál es la diferencia de fondo entre la búsqueda de Material 3 y la de SwiftUI?
2.
SearchBarno recuerda si está expandida. ¿Quién guarda esa bandera en la Pokédex y cómo se entera la barra?3.En Android, el
LargeTopAppBarse envuelve enAnimatedVisibility(visible = !expandida). ¿Por qué?4.La bandera
expandidase guarda conrememberSaveabley no conremember. ¿Qué se evita?5.Con «char» y el teclado abierto, Chimchar y Pecharunt quedaban debajo del teclado y la lista no se desplazaba. ¿Qué arregla
Modifier.imePadding()en laLazyColumn?6.¿Por qué los resultados de la búsqueda se pintan como filas y no como las tarjetas de la rejilla?