Bottom sheet, showModalBottomSheet, StatefulBuilder y Manejo de estado en Flutter
Índice de contenido
- #️⃣ Qué es un Bottom Sheet y cuándo usarlo
- #️⃣ Modal vs Persistent: diferencias reales en la práctica
- #️⃣Cómo crear un Bottom Sheet básico con showModalBottomSheet
- Scroll, tamaño y comportamiento en pantallas grandes
- Mantener estado dentro del Bottom Sheet (StatefulBuilder explicado)
- La Solución: StatefulBuilder
- Bottom Sheets avanzados: diseño, altura y scroll controlado
- #️⃣ DraggableScrollableSheet paso a paso
- #️⃣ Personalización (bordes, radios, colores, safe area)
- #️⃣ Navegación interna dentro del Bottom Sheet (Nested Navigation)
- #️⃣ Navigator con GlobalKey
- #️⃣ Cómo manejar el botón back dentro del sheet
- #️⃣ Buenas prácticas, errores comunes y cómo evitarlos
- Consideraciones al Implementación el Bottom Sheet
- 1. Despliegue con showModalBottomSheet
- 2. Contenido y Estructura
- #️⃣ Conclusión
- #️⃣ Preguntas frecuentes sobre Bottom Sheets en Flutter
Si trabajas con Flutter, tarde o temprano necesitas un Bottom Sheet: ese panel que se desliza desde abajo para mostrar opciones, filtros, formularios o hasta pantallas completas sin abandonar la vista actual.
La buena noticia es que Flutter te lo da prácticamente “de gratis”… y la mala es que si no lo configuras bien, puede romperse el scroll, perder el estado o comportarse distinto según la pantalla (sí, me pasó más de una vez).
En este artículo te cuento cómo crear bottom sheets básicos, avanzados, persistentes, con scroll, con navegación interna, con estado, y de paso te explico un par de comportamientos que descubrí en el día a día mientras los implementaba en producción.
Cuando iniciamos en el desarrollo en Flutter, nos damos cuenta rápidamente de que nuestros Scaffold como nuestros contenedores principales de una app en Flutter, se quedan pequeños rápidamente, y es el dilema que tenemos en las apps móviles, que NO tenemos mucho espacio y por lo tanto, tenemos que utilizar toda clase de técnicas o estructuras como ventanas emergentes, drawers, tabs o en este caso un Bottom Sheet, para presentar esos datos que solicita nuestro usuario en base a alguna acción.
#️⃣ Qué es un Bottom Sheet y cuándo usarlo
Un Bottom Sheet es un panel que aparece desde la parte inferior de la pantalla. Sirve para mostrar contenidos secundarios sin navegar a una nueva ruta.
Lo uso muchísimo para filtros, ajustes rápidos o acciones contextuales. Algo curioso: cuando no tengo el emulador abierto, el bottom sheet tiende a ocupar toda la pantalla; pero en pantallas grandes solo ocupa una parte, lo cual se puede personalizar.
#️⃣ Modal vs Persistent: diferencias reales en la práctica
Modal Bottom Sheet
Bloquea la interacción con el resto de la interfaz y se cierra tocando fuera, deslizando hacia abajo o con Navigator.pop. Ideal para acciones importantes.
Persistent Bottom Sheet
Permite seguir interactuando con la pantalla principal. Es perfecto para menús, mini players o widgets que “acompañan” al usuario.
#️⃣Cómo crear un Bottom Sheet básico con showModalBottomSheet
La forma más común de abrir uno es usando:
showModalBottomSheet(
context: context,
builder: (context) => TuWidget(),
);Y asociarlo a un FloatingActionButton o cualquier cosa que le puedas dar un click o evento del usuario:
FloatingActionButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (_) => _bottomSheetFilter(),
);
},
child: const Icon(Icons.filter_alt_rounded),
);Scroll, tamaño y comportamiento en pantallas grandes
Tuve que envolver mi contenido en un SingleChildScrollView porque al meter muchas categorías, el panel podía romper la UI. Además, Flutter ajusta la altura automáticamente, pero si quieres más control, puedes usar:
isScrollControlled: true,Esto permite que el bottom sheet ocupe más espacio, incluso casi toda la pantalla si lo necesitas.
Mantener estado dentro del Bottom Sheet (StatefulBuilder explicado)
Si tienes un switch, textfield o categoría seleccionable en el Bottom Sheet , ese estado no se comparte automáticamente con el exterior, tienes que crear un estado dentro del Bottom Sheet; para ello se emplea el widget StatefulBuilder:
StatefulBuilder(
builder: (context, setState) {
return ...
}
)Un ejemplo completo:
SingleChildScrollView _bottomSheetFilter() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulBuilder(
// height: 200,
// color: Colors.white,
builder: (BuildContext context, setState) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
children: [
const SizedBox(
height: 20,
),
Text(LocaleKeys.filters.tr(),
style: Theme.of(context)
.textTheme
.displayMedium!
.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(
height: 10,
),
TextField(
controller: _searchController,
textInputAction: TextInputAction.search,
onSubmitted: (value) {
setState(() {
_dataList();
setState(() {});
});
},
decoration: InputDecoration(
labelText: LocaleKeys.searchPost.tr(),
// labelText:
// 'Buscar publicación... (Enter para enviar)',
border: const OutlineInputBorder(),
),
),
ListTile(
leading: const Icon(Icons.language, color: Colors.purple),
title: const Text('Solo en español'),
trailing: Switch.adaptive(
value: _postOnlyLang == 'spanish',
activeColor: Colors.purple,
onChanged: (value) {
_postOnlyLang = 'spanish';
_dataList();
setState(() {});
}),
),
ListTile(
leading: const Icon(Icons.language, color: Colors.purple),
title: const Text('Only in english'),
trailing: Switch.adaptive(
value: _postOnlyLang == 'english',
activeColor: Colors.purple,
onChanged: (value) {
_postOnlyLang = 'english';
_dataList();
setState(() {});
}),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Container(
height: 1, color: Theme.of(context).primaryColor),
),
ListTile(
leading: const Icon(Icons.list, color: Colors.purple),
title: Text(LocaleKeys.categories.tr()),
),
Wrap(
spacing: 1,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
children: categories
.map((c) => TextButton(
child: Text(
c.title,
style: TextStyle(
color: _categoryId == c.id
? Colors.purple
: Theme.of(context)
.colorScheme
.secondary,
fontWeight: _categoryId == c.id
? FontWeight.bold
: FontWeight.w500),
),
onPressed: () {
if (_categoryId == c.id) {
_categoryId = 0;
} else {
_categoryId = c.id;
}
_dataList();
setState(() {});
},
))
.toList(),
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(LocaleKeys.close.tr()),
),
],
),
),
),
),
);
}Por ejemplo, cuando cambio un idioma con un Switch.adaptive, tengo que llamar a setState() dentro del StatefulBuilder porque el bottom sheet es una “página aparte”.
Es una técnica sencilla pero imprescindible si no quieres que tus filtros se vean congelados.
El Bottom Sheet (o Hoja Inferior) se comporta como si fuera una página aparte, y no como una dependencia de la pantalla principal, especialmente en lo que respecta a la gestión del estado.
Esto significa que, para que las operaciones internas (como cambiar un Switch o interactuar con un TextField) reflejen cambios visuales, debemos actualizar su estado de forma explícita.
La Solución: StatefulBuilder
Para lograr que los cambios se muestren dentro del Bottom Sheet (por ejemplo, al cambiar el valor del Switch), debemos:
- Gatillar la Actualización: Ejecutar la función setState cada vez que se realiza una operación (en tu caso, al cambiar el valor del Switch).
- Envolver en StatefulBuilder: Para que setState funcione dentro del Bottom Sheet y solo actualice esa área, hay que emplear el widget StatefulBuilder en la raíz del contenido del Bottom Sheet.
Esto permite que el Bottom Sheet maneje su propio estado localmente, sin forzar la actualización de toda la página que lo contiene.
Bottom Sheets avanzados: diseño, altura y scroll controlado
Cuando necesitas un comportamiento más fino (como arrastrar el panel hacia arriba), Flutter ofrece:
#️⃣ DraggableScrollableSheet paso a paso
Ejemplo:
DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.2,
maxChildSize: 0.9,
builder: (_, controller) {
return ListView(
controller: controller,
children: [...]
);
},
);Esto te permite crear sheets “semi flotantes”, parecidos a los de Google Maps.
#️⃣ Personalización (bordes, radios, colores, safe area)
Puedes ajustar la forma con:
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
clipBehavior: Clip.antiAliasWithSaveLayer,Una recomendación personal: cuando usas teclado dentro del sheet, activa:
useSafeArea: trueO verás cómo el teclado “muerde” la parte inferior.
#️⃣ Navegación interna dentro del Bottom Sheet (Nested Navigation)
Hay casos donde el bottom sheet necesita tener sus propias pantallas (por ejemplo, una lista → detalle).
Es completamente posible gracias a un Navigator interno.
#️⃣ Navigator con GlobalKey
final navigatorKey = GlobalKey<NavigatorState>();Luego:
Navigator(
key: navigatorKey,
onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => Page1()),
)#️⃣ Cómo manejar el botón back dentro del sheet
Aquí Flutter no hace magia.
- Tú decides qué pasa cuando el usuario presiona "Back":
- Si el navegador interno puede hacer pop → haces pop.
- Si no → cierras el bottom sheet.
Esta lógica mejora muchísimo la UX y evita que el usuario salga del sheet antes de tiempo.
#️⃣ Buenas prácticas, errores comunes y cómo evitarlos
No cargues demasiado contenido dentro del sheet. Afecta rendimiento.
- Usa StatefulBuilder cuando solo necesitas estado local.
- Para UI complejas, crea un StatefulWidget dedicado.
- No abuses del tamaño completo; deja claro que el usuario sigue en la misma pantalla.
Prueba en varios dispositivos. A mí me pasó que el mismo bottom sheet ocupaba “toda la pantalla” en unos equipos y solo un recuadro en otros.
Consideraciones al Implementación el Bottom Sheet
Crear y desplegar un Bottom Sheet (o Hoja Inferior) es un proceso muy sencillo en Flutter. En este caso, lo he asociado al botón flotante (FloatingActionButton) de nuestro Scaffold.
1. Despliegue con showModalBottomSheet
Para mostrar el Bottom Sheet, utilizamos el método nativo de Flutter: showModalBottomSheet.
- Disponibilidad: Este método ya viene "de gratis" en la API de Flutter; solo hay que invocarlo.
- Argumentos: Recibe dos parámetros principales:
- context: El contexto actual del widget.
- builder: Una función que debe retornar el widget que será el contenido de la Hoja Inferior.
2. Contenido y Estructura
El builder puede retornar cualquier widget. En este caso, el contenido ha sido modularizado en un widget separado (CustomSwitch y Filter).
- SingleChildScrollView: Es una buena práctica envolver el contenido dentro de un SingleChildScrollView. Esto garantiza que si el contenido excede el espacio disponible, se habilitará el scroll y la aplicación no se romperá por desbordamiento (overflow).
- Personalización: Dentro de este scroll, puedes colocar cualquier contenido que necesites, como formularios, listas de opciones o filtros.
3. Naturaleza del Bottom Sheet
Un punto crucial es entender cómo se comporta el Bottom Sheet respecto al estado de la aplicación:
- Página Adicional: El Bottom Sheet se comporta como si fuera una página adicional o un widget completamente anexo al Scaffold padre.
- Estado Separado: Esto implica que tiene un estado completamente distinto. Si deseas que los widgets internos (como un Switch) se actualicen visualmente al interactuar con ellos, deberás gestionar su estado de manera explícita (como se explicó en la clase anterior con el StatefulBuilder).
#️⃣ Conclusión
Los Bottom Sheets en Flutter son increíblemente poderosos.
Desde un panel simple con un filtro hasta una pantalla completa con navegación interna, puedes construir casi cualquier cosa.
La clave está en entender:
- Modal vs Persistent
- Estado interno
- Scroll y altura
- Navegación anidada
- Personalización visual
Una vez dominas esto, puedes crear experiencias realmente fluidas sin necesidad de rutas adicionales.
#️⃣ Preguntas frecuentes sobre Bottom Sheets en Flutter
- ¿Cómo evitar que el bottom sheet se cierre al tocar afuera?
- isDismissible: false.
- ¿Cómo mantener estado dentro del sheet?
- StatefulBuilder o un widget con estado.
- ¿Cómo personalizar altura y forma?
- isScrollControlled, RoundedRectangleBorder, clipBehavior.
- ¿Cómo evitar que el teclado tape el contenido?
- useSafeArea: true.
- ¿Puedo navegar dentro del bottom sheet?
- Sí, con un Navigator interno + GlobalKey.
Aprende a enviar Solicitudes HTTP en Flutter.
Acepto recibir anuncios de interes sobre este Blog.
Veremos como utilizar los Modal de tipo Bottom Sheet en Flutter mediante showModalBottomSheet y algunas consideraciones adicionales.