En el anterior artículo, aprendimos a crear nuestra primera aplicación básica en Flutter. En este, profundizaremos un poco más, veremos conceptos como la navegación entre pantallas, mostrar información, widgets, animaciones y conexión a internet. ¡Comenzamos!
Si no has leído la primera parte de esta serie de artículos, haz clic en la siguiente imagen.
Índice de contenidos
Widgets en Flutter
Como adelantamos en el artículo anterior, los widgets son bloques de construcción fundamentales en Flutter. Todo es un widget en Flutter. Tenemos varios tipos, ¡vamos a verlos!
Inherited widgets
Los inherited widgets, nos permiten compartir datos a través del árbol de widgets. Son muy útiles para la gestión de estados.
class ThemeData extends InheritedWidget {
final Color primaryColor;
ThemeData({
required this.primaryColor,
required Widget child
}) : super(child: child);
static ThemeData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeData>()!;
}
@override
bool updateShouldNotify(ThemeData oldWidget) {
return primaryColor != oldWidget.primaryColor;
}
}
Stateless widgets
Son widgets inmutables, lo que significa que sus propiedades no pueden cambiar durante su vida útil. Son perfectos para partes de la interfaz que no necesitan mantener un estado, como:
class WelcomeText extends StatelessWidget {
final String name;
WelcomeText({required this.name});
@override
Widget build(BuildContext context) {
return Text('¡Bienvenido $name!');
}
}
Stateful widgets
Estos widgets pueden mantener un estado que puede cambiar durante el tiempo de vida del widget. Son útiles para componentes interactivos como un contador.
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Contador: $count'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Incrementar')
)
]
);
}
}

Styled widgets
Son widgets que te permiten personalizar la apariencia de tus componentes. Por ejemplo:
- Container (para decoración y padding)
- DecoratedBox
- Transform
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 2),
blurRadius: 6
)
]
),
padding: EdgeInsets.all(16),
child: Text('Widget Estilizado')
)
Material widgets

Son widgets que implementan el diseño Material Design de Google. Incluyen componentes como:
- AppBar
- FloatingActionButton
- Card
- Drawer

Cupertino widgets
Los Cupertino son widgets que siguen las guías de diseño de iOS. Incluyen:
- CupertinoNavigationBar
- CupertinoButton
- CupertinoActionSheet
La navegación es uno de los conceptos más importantes en el desarrollo de aplicaciones móviles. En Flutter, podemos manejar la navegación de forma sencilla utilizando el widget Navigator. Veamos paso a paso cómo implementar diferentes tipos de navegación.
Crear múltiples pantallas
Primero, necesitamos crear diferentes pantallas para navegar entre ellas. Cada pantalla será una clase que extiende de StatelessWidget o StatefulWidget.
// home_screen.dart
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Inicio'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// Aquí implementaremos la navegación
},
child: Text('Ir a detalles'),
),
),
);
}
}
// details_screen.dart
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detalles'),
),
body: Center(
child: Text('Pantalla de detalles'),
),
);
}
}

Flutter utiliza un sistema de navegación basado en pila (stack). Las operaciones básicas son:
push
: añade una nueva pantalla encima de la actualpop
: regresa a la pantalla anterior
// Navegación hacia adelante (push)
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsScreen()),
);
},
child: Text('Ir a detalles'),
);
// Navegación hacia atrás (pop)
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Volver'),
);
Pasar datos entre pantallas
Es común necesitar pasar datos de una pantalla a otra. Podemos hacerlo a través del constructor de la pantalla a la que pasamos información.
// details_screen.dart con parámetros
class DetailsScreen extends StatelessWidget {
final String title;
final String description;
const DetailsScreen({
required this.title,
required this.description,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text(description),
),
);
}
}
// Navegación con paso de datos
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsScreen(
title: 'Mi título',
description: 'Esta es la descripción',
),
),
);
},
child: Text('Ver detalles'),
);
Una forma común de navegación en aplicaciones móviles es usar una barra de navegación inferior.

class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0;
static const List<Widget> _screens = <Widget>[
HomeScreen(),
SearchScreen(),
ProfileScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _screens.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Inicio',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Buscar',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Perfil',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
Puntos importantes
Manejo del botón atrás: Flutter maneja automáticamente el botón atrás en Android cuando usas Navigator.push().
Rutas con nombre: Para aplicaciones más grandes, podemos usar rutas con nombre para facilitarnos la navegación.
// En MaterialApp
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailsScreen(),
'/profile': (context) => ProfileScreen(),
},
);
// Navegar usando rutas con nombre
Navigator.pushNamed(context, '/details');
Navegación con resultado: Puedes esperar un resultado cuando regresas de una pantalla:
// Navegar y esperar resultado
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// En la pantalla de selección
Navigator.pop(context, 'Resultado seleccionado');
Mostrar datos en tu aplicación Flutter
Flutter ofrece varias formas de mostrar datos de manera eficiente y atractiva, estas son las más comunes.
ListView
ListView es el widget más común para mostrar listas. Tiene varias formas de implementación:

ListView.builder: Ideal para listas largas o infinitas, ya que construye los elementos solo cuando son visibles.
class ProductList extends StatelessWidget {
final List<Product> products = [
Product(name: 'Laptop', price: 999.99),
Product(name: 'Mouse', price: 29.99),
Product(name: 'Teclado', price: 59.99),
// ... más productos
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.shopping_bag),
title: Text(products[index].name),
subtitle: Text('\$${products[index].price}'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
// Manejar el tap
},
);
},
);
}
}
ListView.separated: Similar a ListView.builder, pero permite añadir un separador entre elementos.
ListView.separated(
itemCount: products.length,
separatorBuilder: (context, index) => Divider(
color: Colors.grey[300],
height: 1,
),
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index].name),
subtitle: Text('\$${products[index].price}'),
);
},
);
GridView

GridView organiza los elementos en una cuadrícula. Es perfecto para galerías de imágenes o catálogos.
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Número de columnas
crossAxisSpacing: 10.0, // Espacio horizontal entre items
mainAxisSpacing: 10.0, // Espacio vertical entre items
childAspectRatio: 1.0, // Relación de aspecto de cada item
),
padding: EdgeInsets.all(10.0),
itemCount: products.length,
itemBuilder: (context, index) {
return Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(products[index].imageUrl, height: 100),
SizedBox(height: 8),
Text(products[index].name),
Text('\$${products[index].price}'),
],
),
);
},
);
Otras maneras de mostrar información en Flutter
También podemos mostrar información en diferentes elementos o widgets como tarjetas. Puedes verlas todas en el catálogo de widgets de Flutter.

Card(
child: Container(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
product.imageUrl,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
Padding(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'\$${product.price}',
style: TextStyle(
color: Colors.green,
fontSize: 16,
),
),
],
),
),
],
),
),
);
Animaciones Básicas en tu app Flutter
Flutter ofrece varias formas sencillas de añadir animaciones a tu app. Recomendamos mirar el catálogo de animaciones de Flutter, donde podrás ver cómo funciona cada una de ellas de manera muy visual.
Animaciones implícitas
Las animaciones implícitas son la forma más sencilla de animar widgets en Flutter. El framework se encarga automáticamente de la animación cuando cambias ciertos valores.
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
width: _isExpanded ? 200.0 : 100.0,
height: _isExpanded ? 200.0 : 100.0,
color: _isExpanded ? Colors.blue : Colors.red,
curve: Curves.easeInOut,
child: Center(
child: Text(
'Haz click aquí',
style: TextStyle(color: Colors.white),
),
),
),
Efectos de transición
Las animaciones Hero son perfectas para transiciones entre pantallas:
// En la primera pantalla
Hero(
tag: 'imageHero',
child: Image.network(
'url_de_la_imagen',
height: 100,
),
)
// En la pantalla de destino
Hero(
tag: 'imageHero',
child: Image.network(
'url_de_la_imagen',
height: 300,
),
)
Animaciones de loading
Puedes añadir una animación para cuando la aplicación está cargando, de esta manera mejorarás la experiencia del usuario.
Conexión a Internet desde una app Flutter
La conectividad a internet es una parte fundamental de casi cualquier aplicación móvil moderna. Flutter proporciona herramientas para manejar las comunicaciones en red de manera eficiente y segura. Veamos los aspectos más importantes:
Realizar peticiones HTTP
Para realizar peticiones HTTP en Flutter, necesitamos utilizar el paquete `http`. Este paquete proporciona una manera sencilla y efectiva de comunicarse con servicios web. Primero, debemos añadirlo a nuestro proyecto a través del archivo pubspec.yaml
dependencies:
http: ^1.1.0
Las peticiones HTTP son operaciones asíncronas, lo que significa que necesitamos esperar a que se complete la operación antes de procesar los datos. Flutter maneja esto con el uso de async y await.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _data = "Cargando...";
// Método para realizar una petición HTTP GET
Future<void> fetchData() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
setState(() {
_data = responseData['title'];
});
} else {
setState(() {
_data = "Error: ${response.statusCode}";
});
}
} catch (e) {
setState(() {
_data = "Error al conectar: $e";
});
}
}
@override
void initState() {
super.initState();
fetchData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Conexión a Internet'),
),
body: Center(
child: Text(
_data,
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}
}
Consumir una API REST simple desde Flutter
Las APIs REST son la forma más común de comunicación entre aplicaciones móviles y servidores. Cuando trabajamos con APIs REST, es una buena práctica crear modelos que representen nuestros datos. Esto nos ayuda a mantener el código organizado y seguro.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// Modelo para representar los datos
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
// Factory para crear un Post desde un JSON
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<Post> _post;
// Método para obtener datos de la API
Future<Post> fetchPost() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
final response = await http.get(url);
if (response.statusCode == 200) {
return Post.fromJson(json.decode(response.body));
} else {
throw Exception('Error al cargar los datos');
}
}
@override
void initState() {
super.initState();
_post = fetchPost();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Consumiendo API REST'),
),
body: FutureBuilder<Post>(
future: _post,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final post = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ID: ${post.id}',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
SizedBox(height: 10),
Text(
'Título: ${post.title}',
style: TextStyle(fontSize: 18),
),
SizedBox(height: 10),
Text(
'Contenido: ${post.body}',
style: TextStyle(fontSize: 16),
),
],
),
);
} else {
return Center(child: Text('No se encontraron datos'));
}
},
),
);
}
}
Mostrar datos desde internet
Cuando trabajamos con datos de internet, debemos considerar que las peticiones pueden tardar y pueden fallar. Flutter proporciona el widget FutureBuilder específicamente para manejar estas situaciones. El FutureBuilder permite:
- Mostrar un indicador de carga mientras esperamos los datos
- Manejar errores
- Actualizar la UI automáticamente cuando llegan los datos
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// Modelo para representar los datos
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
// Factory para crear un User desde un JSON
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Método para obtener datos desde la API
Future<List<User>> fetchUsers() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/users');
final response = await http.get(url);
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Error al cargar los usuarios');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mostrar Datos desde Internet'),
),
body: FutureBuilder<List<User>>(
future: fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Indicador de carga
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// Mostrar mensaje de error
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
// Mostrar lista de usuarios
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
child: Text(user.id.toString()),
),
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
} else {
// Mostrar mensaje si no hay datos
return Center(child: Text('No se encontraron usuarios'));
}
},
),
);
}
}
¡Ya está!
¡Perfecto! Ahora ya puedes incorporar elementos y funcionalidades más interesantes a tu aplicación Flutter. ¿Tienes algún proyecto Flutter en mente? ¡Cuéntanos en comentarios!
Deja una respuesta
Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *