Flutter para desarrolladores de UIKit
Aprende cómo aplicar los conocimientos de desarrollo de iOS y UIKit al crear aplicaciones de Flutter.
Los desarrolladores de iOS con experiencia en el uso de UIKit que quieran escribir aplicaciones móviles usando Flutter deberían revisar esta guía. Explica cómo aplicar los conocimientos existentes de UIKit a Flutter.
Flutter es un framework para compilar aplicaciones multiplataforma que utiliza el lenguaje de programación Dart. Para entender algunas diferencias entre programar con Dart y programar con Swift, consulta Aprender Dart como desarrollador de Swift y Concurrencia de Flutter para desarrolladores de Swift.
Tus conocimientos y experiencia en iOS y UIKit son muy valiosos al compilar con Flutter. Flutter también realiza una serie de adaptaciones al comportamiento de la aplicación cuando se ejecuta en iOS. Para saber cómo, consulta Adaptaciones de la plataforma.
Usa esta guía como un libro de recetas. Salta de un lado a otro y encuentra las preguntas que aborden tus necesidades más relevantes.
Resumen
#Como introducción, mira el siguiente video. Describe a grandes rasgos cómo funciona Flutter en iOS y cómo usar Flutter para compilar aplicaciones de iOS.
Views vs. Widgets
#En UIKit, la mayor parte de lo que creas en la UI se hace usando objetos de vista,
que son instancias de la clase UIView.
Estos pueden actuar como contenedores para otras clases UIView,
que forman tu layout.
En Flutter, el equivalente aproximado a una UIView es un Widget.
Los Widgets no se mapean exactamente a las vistas de iOS,
but mientras te familiarizas con cómo funciona Flutter,
puedes pensar en ellos como "la forma en que declaras y construyes la UI".
Sin embargo, estos tienen algunas diferencias con una UIView.
Para empezar, los widgets tienen una vida útil diferente: son inmutables
y solo existen hasta que necesitan ser cambiados.
Cada vez que los widgets o su State cambian,
el framework de Flutter crea un nuevo árbol de instancias de widgets.
En comparación, una vista de UIKit no se recrea cuando cambia,
sino que es una entidad mutable que se dibuja una vez
y no se vuelve a dibujar hasta que se invalida mediante setNeedsDisplay().
Además, a diferencia de UIView, los widgets de Flutter son ligeros,
en parte debido a su inmutabilidad.
Esto se debe a que no son vistas en sí mismos,
y no dibujan nada directamente,
sino que son una descripción de la UI y su semántica
que se "infla" en objetos de vista reales bajo el capó.
Flutter incluye la biblioteca Material Components. Estos son widgets que implementan las directrices de Material Design. Material Design es un sistema de diseño flexible optimizado para todas las plataformas, incluyendo iOS.
Pero Flutter es lo suficientemente flexible y expresivo para implementar cualquier lenguaje de diseño. En iOS, puedes usar la biblioteca de widgets de Cupertino para producir una interfaz que se parezca al lenguaje de diseño iOS de Apple.
Actualizar widgets
#Para actualizar tus vistas en UIKit, las mutas directamente. En Flutter, los widgets son inmutables y no se actualizan directamente. En su lugar, tienes que manipular el State del widget.
Aquí es donde entra el concepto de widgets Stateful vs Stateless
cuando se compila. Un StatelessWidget es justo lo que parece:
un widget sin ningún State asociado.
Los StatelessWidgets son útiles cuando la parte de la interfaz de usuario que estás
describiendo no depende de nada más que de la información de configuración inicial
en el widget.
Por ejemplo, con UIKit, esto es similar a colocar un UIImageView
con tu logo como la imagen. Si el logo no va a cambiar durante el tiempo de ejecución,
usa un StatelessWidget en Flutter.
Si quieres cambiar dinámicamente la UI según los datos recibidos
después de realizar una llamada HTTP, usa un StatefulWidget.
Una vez finalizada la llamada HTTP, indícale al framework de Flutter
que el State del widget se ha actualizado, para que pueda actualizar la UI.
La diferencia importante entre los widgets stateless y
stateful es que los StatefulWidgets tienen un objeto State
que almacena los datos de State y los mantiene a través de las reconstrucciones del árbol,
por lo que no se pierden.
Si tienes dudas, recuerda esta regla:
si un widget cambia fuera del método build
(por ejemplo, debido a las interacciones del usuario en tiempo de ejecución),
es stateful.
Si el widget nunca cambia una vez compilado, es stateless.
Sin embargo, incluso si un widget es stateful, el widget padre contenedor
aún puede ser stateless si no reacciona por sí mismo a esos cambios
(u otras entradas).
El siguiente ejemplo muestra cómo usar un StatelessWidget.
Un StatelessWidget común es el widget Text.
Si miras la implementación del widget Text,
verás que es una subclase de StatelessWidget.
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
Si miras el código anterior, notarás que el widget Text
no lleva ningún State explícito con él. Renderiza lo que se pasa en sus
constructores y nada más.
Pero, ¿qué pasa si quieres hacer que "I Like Flutter" cambie dinámicamente,
por ejemplo, al hacer clic en un FloatingActionButton?
Para lograr esto, envuelve el widget Text en un StatefulWidget y
actualízalo cuando el usuario haga clic en el botón.
Por ejemplo:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default placeholder text
String textToShow = 'I Like Flutter';
void _updateText() {
setState(() {
// Update the text
textToShow = 'Flutter is Awesome!';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
Layout de widgets
#En UIKit, podrías usar un archivo Storyboard para organizar tus vistas y establecer restricciones (constraints), o podrías establecer tus restricciones mediante programación en tus controladores de vista. En Flutter, declaras tu layout en código componiendo un árbol de widgets.
El siguiente ejemplo muestra cómo mostrar un widget simple con padding:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: CupertinoButton(
onPressed: () {},
padding: const EdgeInsets.only(left: 10, right: 10),
child: const Text('Hello'),
),
),
);
}
Puedes agregar padding a cualquier widget, lo que imita la funcionalidad de las restricciones (constraints) en iOS.
Puedes ver los layouts que ofrece Flutter en el catálogo de widgets.
Eliminar Widgets
#En UIKit, llamas a addSubview() en el padre,
o a removeFromSuperview() en una vista hija
para agregar o eliminar vistas hijas dinámicamente.
En Flutter, debido a que los widgets son inmutables,
no existe un equivalente directo a addSubview().
En su lugar, puedes pasar una función al padre
que devuelva un widget, y controlar la creación de ese hijo
con una bandera booleana.
El siguiente ejemplo muestra cómo alternar entre dos widgets
cuando el usuario hace clic en el FloatingActionButton:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text('Toggle One');
}
return CupertinoButton(onPressed: () {}, child: const Text('Toggle Two'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: _getToggleChild()),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
Animaciones
#En UIKit, creas una animación llamando al
método animate(withDuration:animations:) en una vista.
En Flutter, usa la biblioteca de animación
para envolver los widgets dentro de un widget animado.
En Flutter, usa un AnimationController, que es un Animation<double>
que puede pausar, buscar, detener e invertir la animación.
Requiere un Ticker que indique cuándo ocurre la sincronización vertical (vsync)
y produzca una interpolación lineal
entre 0 and 1 en cada fotograma mientras se está ejecutando.
Luego, creas una o más
Animations y las adjuntas al controlador.
Por ejemplo, puedes usar CurvedAnimation
para implementar una animación a lo largo de una curva interpolada.
En este sentido, el controlador es la fuente "maestra"
del progreso de la animación
y la CurvedAnimation calcula la curva
que reemplaza el movimiento lineal predeterminado del controlador.
Al igual que los widgets, las animaciones en Flutter funcionan con composición.
Al construir el árbol de widgets, asignas la Animation a una
propiedad animada de un widget, como la opacidad de un FadeTransition,
e indicas al controlador que inicie la animación.
El siguiente ejemplo muestra cómo escribir una FadeTransition que
desvanece el widget hacia un logo cuando presionas el FloatingActionButton:
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Fade Demo',
home: MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
const MyFadeTest({super.key, required this.title});
final String title;
@override
State<MyFadeTest> createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: FadeTransition(
opacity: curve,
child: const FlutterLogo(size: 100),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward();
},
tooltip: 'Fade',
child: const Icon(Icons.brush),
),
);
}
}
Para más información, consulta Widgets de animación y movimiento, el tutorial de animaciones, y la descripción general de animaciones.
Dibujar en la pantalla
#En UIKit, usas CoreGraphics para dibujar líneas y formas en la
pantalla. Flutter tiene una API diferente basada en la clase Canvas,
con otras dos clases que te ayudan a dibujar: CustomPaint y CustomPainter,
siendo esta última la que implementa tu algoritmo para dibujar en el lienzo (canvas).
Para aprender a implementar un pintor de firmas en Flutter, consulta la respuesta de Collin en StackOverflow.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
const Signature({super.key});
@override
State<Signature> createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset?>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
Offset localPosition = referenceBox.globalToLocal(
details.globalPosition,
);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child: CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
}
}
@override
bool shouldRepaint(SignaturePainter oldDelegate) =>
oldDelegate.points != points;
}
Opacidad del widget
#En UIKit, todo tiene .opacity o .alpha.
En Flutter, la mayoría de las veces necesitas
envolver un widget en un widget Opacity para lograr esto.
Widgets personalizados
#En UIKit, normalmente creas una subclase de UIView, o usas una vista preexistente,
para sobrescribir e implementar métodos que logren el comportamiento deseado.
En Flutter, construyes un widget personalizado componiendo
widgets más pequeños
(en lugar de extenderlos).
Por ejemplo, ¿cómo se construye un CustomButton
que recibe una etiqueta en el constructor?
Crea un CustomButton que componga un ElevatedButton con una etiqueta,
en lugar de extender ElevatedButton:
class CustomButton extends StatelessWidget {
const CustomButton(this.label, {super.key});
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}
Luego usa CustomButton,
tal como usarías cualquier otro widget de Flutter:
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}
Administrar dependencias
#En iOS, agregas dependencias con CocoaPods añadiéndolas a tu Podfile.
Flutter utiliza el sistema de compilación de Dart y el gestor de paquetes Pub
para manejar las dependencias. Las herramientas delegan la compilación de las
aplicaciones contenedor de Android e iOS nativas a los
respectivos sistemas de compilación.
Aunque hay un Podfile en la carpeta de iOS en tu
proyecto de Flutter, solo úsalo si estás agregando dependencias
nativas necesarias para la integración específica de cada plataforma.
En general, utiliza pubspec.yaml para declarar dependencias externas en Flutter.
Un buen lugar para encontrar paquetes excelentes para Flutter es en pub.dev.
Navegación
#Esta sección del documento analiza la navegación entre páginas de una aplicación, el mecanismo de push y pop, y más.
Navegar entre páginas
#En UIKit, para navegar entre controladores de vista, puedes usar un
UINavigationController que gestiona la pila de controladores de vista
a mostrar.
Flutter tiene una implementación similar,
utilizando un Navigator y Routes.
Una Route es una abstracción para una "pantalla" o "página" de una aplicación,
y un Navigator es un widget
que gestiona rutas. Una ruta se mapea aproximadamente a un
UIViewController. El navigator funciona de manera similar al
UINavigationController de iOS, en el sentido de que puede realizar push() y pop()
rutas dependiendo de si deseas navegar hacia o desde una vista.
Para navegar entre páginas, tienes un par de opciones:
- Especifica un
Mapde nombres de rutas. - Navegar directamente a una ruta.
El siguiente ejemplo compila un Map.
void main() {
runApp(
CupertinoApp(
home: const MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder>{
'/a': (context) => const MyPage(title: 'page A'),
'/b': (context) => const MyPage(title: 'page B'),
'/c': (context) => const MyPage(title: 'page C'),
},
),
);
}
Navega a una ruta haciendo push de su nombre en el Navigator.
Navigator.of(context).pushNamed('/b');
La clase Navigator maneja el enrutamiento en Flutter y se utiliza para recuperar
un resultado de una ruta a la que has hecho push en la pila.
Esto se logra haciendo await en el Future devuelto por push().
Por ejemplo, para iniciar una ruta de location que permita al usuario seleccionar su
ubicación, podrías hacer lo siguiente:
Object? coordinates = await Navigator.of(context).pushNamed('/location');
Y luego, dentro de tu ruta de location, una vez que el usuario haya seleccionado su
ubicación, realiza un pop() en la pila con el resultado:
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
Navegar a otra aplicación
#En UIKit, para enviar al usuario a otra aplicación,
utilizas un esquema de URL específico.
Para las aplicaciones a nivel de sistema, el esquema depende de la aplicación.
Para implementar esta funcionalidad en Flutter,
crea una integración de plataforma nativa, o usa un
plugin existente, como url_launcher.
Regresar manualmente (pop back)
#Llamar a SystemNavigator.pop() desde tu código de Dart
invoca el siguiente código de iOS:
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
[((UINavigationController*)viewController) popViewControllerAnimated:NO];
}
Si eso no hace lo que deseas, puedes crear tu propio canal de plataforma (platform channel) para invocar código iOS arbitrario.
Manejar la localización
#A diferencia de iOS, que tiene el archivo Localizable.strings,
Flutter no tiene actualmente un sistema dedicado para manejar strings.
Por el momento, la mejor práctica es declarar tus textos
en una clase como campos estáticos y acceder a ellos desde allí. Por ejemplo:
class Strings {
static const String welcomeMessage = 'Welcome To Flutter';
}
Puedes acceder a tus strings de esta manera:
Text(Strings.welcomeMessage);
Por defecto, Flutter solo admite inglés de EE. UU. para sus strings.
Si necesitas agregar soporte para otros idiomas,
incluye el paquete flutter_localizations.
También es posible que necesites agregar el paquete intl de Dart
para usar el mecanismo de localización (i18n), como el formato de fecha/hora.
dependencies:
flutter_localizations:
sdk: flutter
intl: any # Use version of intl from flutter_localizations.
Para usar el paquete flutter_localizations,
especifica los localizationsDelegates y
supportedLocales en el widget de la aplicación:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
// Add app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: <Locale>[
Locale('en', 'US'), // English
Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
);
}
}
Los delegados contienen los valores localizados reales,
mientras que supportedLocales define qué configuraciones regionales (locales) admite la aplicación.
El ejemplo anterior utiliza un MaterialApp,
por lo que tiene tanto un GlobalWidgetsLocalizations
para los valores localizados de los widgets base,
como un MaterialWidgetsLocalizations para las localizaciones de los widgets de Material.
Si utilizas WidgetsApp para tu aplicación, no necesitas este último.
Ten en cuenta que estos dos delegados contienen valores "predeterminados",
pero deberás proporcionar uno o más delegados
para los textos localizables de tu propia aplicación,
si deseas que también se localicen.
Cuando se inicializa, WidgetsApp (o MaterialApp)
crea un widget Localizations
para ti,
con los delegados que especifiques.
La configuración regional (locale) actual del dispositivo siempre es accesible
desde el widget Localizations desde el contexto actual
(en forma de un objeto Locale), o utilizando Window.locale.
Para acceder a los recursos localizados, usa el método Localizations.of()
para acceder a una clase de localización específica que proporciona un delegado determinado.
Usa el paquete intl_translation
para extraer los textos traducibles
a archivos arb para su traducción, e impórtalos de vuelta a la aplicación
para usarlos con intl.
Para obtener más detalles sobre la internacionalización y localización en Flutter,
consulta la guía de internacionalización, que tiene código de ejemplo
con y sin el paquete intl.
ViewControllers
#Esta sección del documento analiza el equivalente de ViewController en Flutter y cómo escuchar eventos del ciclo de vida.
Equivalente de ViewController en Flutter
#En UIKit, un ViewController representa una parte de la interfaz de usuario,
utilizada con mayor frecuencia para una pantalla o sección.
Estos se componen para construir interfaces de usuario complejas
y ayudar a escalar la UI de tu aplicación.
En Flutter, este trabajo corresponde a los Widgets.
Como se mencionó en la sección de navegación,
las pantallas en Flutter están representadas por Widgets, ya que
"¡todo es un widget!"
Usa un Navigator para moverte entre diferentes Routes
que representan diferentes pantallas o páginas,
o tal vez diferentes estados o representaciones de los mismos datos.
Escuchar eventos del ciclo de vida
#En UIKit, puedes sobrescribir métodos en el ViewController
para capturar métodos del ciclo de vida de la propia vista,
o registrar devoluciones de llamada del ciclo de vida en el AppDelegate.
En Flutter, no tienes ninguno de los dos conceptos, pero en su lugar puedes
escuchar los eventos del ciclo de vida vinculándote al
observador WidgetsBinding y escuchando
el evento de cambio didChangeAppLifecycleState().
Los eventos del ciclo de vida observables son:
inactiveLa aplicación está en un estado inactivo y no está recibiendo
entrada del usuario. Este evento solo funciona en iOS, ya que no existe un evento equivalente en Android.
pausedLa aplicación no está visible actualmente para el usuario,
no responde a la entrada del usuario, pero se está ejecutando en segundo plano.
resumedLa aplicación está visible y responde a la entrada del usuario.
suspendingLa aplicación se suspende momentáneamente.
La plataforma iOS no tiene un evento equivalente.
Para obtener más detalles sobre el significado de estos estados, consulta la
documentación de AppLifecycleState.
Diseños (Layouts)
#Esta sección analiza diferentes layouts en Flutter y cómo se comparan con UIKit.
Mostrar una vista de lista
#En UIKit, podrías mostrar una lista en
un UITableView o en un UICollectionView.
En Flutter, tienes una implementación similar utilizando un ListView.
En UIKit, estas vistas tienen métodos de delegado
para decidir el número de filas,
la celda para cada ruta de índice (index path) y el tamaño de las celdas.
Debido al patrón de widgets inmutables de Flutter,
pasas una lista de widgets a tu ListView,
y Flutter se encarga de asegurarse de que
el desplazamiento (scrolling) sea rápido y fluido.
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
final List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}
Detectar qué se hizo clic
#En UIKit, implementas el método delegado,
tableView:didSelectRowAtIndexPath:.
En Flutter, usa el manejo táctil proporcionado por los widgets pasados.
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}
Actualizar ListView dinámicamente
#En UIKit, actualizas los datos para la vista de lista,
y notificas a la tabla o vista de colección utilizando el
método reloadData.
En Flutter, si actualizas la lista de widgets dentro de un setState(),
notarás rápidamente que tus datos no cambian visualmente.
Esto se debe a que cuando se llama a setState(),
el motor de renderizado de Flutter analiza el árbol de widgets
para ver si algo ha cambiado.
Cuando llega a tu ListView, realiza una comprobación ==,
y determina que los dos ListViews son iguales.
Nada ha cambiado, por lo que no se requiere ninguna actualización.
Para una forma sencilla de actualizar tu ListView,
crea una nueva List dentro de setState(),
y copia los datos de la lista antigua a la nueva lista.
Si bien este enfoque es simple,
no se recomienda para conjuntos de datos grandes,
como se muestra en el siguiente ejemplo.
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = <Widget>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: widgets),
);
}
}
La forma recomendada, eficiente
y efectiva de construir una lista utiliza un ListView.Builder.
Este método es excelente cuando tienes una
lista dinámica o una lista con cantidades muy grandes de datos.
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
}
En lugar de crear un ListView, crea un ListView.builder
que reciba dos parámetros clave: la longitud inicial de la lista
y una función ItemBuilder.
La función ItemBuilder es similar al método delegado cellForItemAt
en una tabla o vista de colección de iOS,
ya que toma una posición y devuelve la
celda que deseas renderizar en esa posición.
Finalmente, pero lo más importante, nota que la función onTap()
ya no recrea la lista, sino que hace .add (añade) en ella.
Crear una vista de desplazamiento
#En UIKit, envuelves tus vistas en un ScrollView que
permite al usuario desplazarse por el contenido si es necesario.
En Flutter, la forma más fácil de hacer esto es usando el widget ListView.
Este actúa tanto como un ScrollView como una TableView de iOS,
ya que puedes disponer los widgets en formato vertical.
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
Para obtener documentos más detallados sobre cómo disponer widgets en Flutter, consulta el tutorial de layout.
Detección de gestos y manejo de eventos táctiles
#Esta sección analiza cómo detectar gestos y manejar diferentes eventos en Flutter, y cómo se comparan con UIKit.
Agregar un listener de clics
#En UIKit, adjuntas un GestureRecognizer a una vista para
manejar eventos de clic.
En Flutter, hay dos formas de agregar listeners táctiles:
- Si el widget admite la detección de eventos, pásale una función,
y maneja el evento en la función. Por ejemplo, el widget
ElevatedButtontiene un parámetroonPressed:
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- Si el Widget no admite la detección de eventos,
envuelve el widget en un GestureDetector y pasa una función
al parámetro
onTap.
class SampleTapApp extends StatelessWidget {
const SampleTapApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
developer.log('tap');
},
child: const FlutterLogo(size: 200),
),
),
);
}
}
Manejar otros gestos
#Usando GestureDetector puedes escuchar
una amplia gama de gestos como:
-
Tapping
onTapDownUn puntero que podría causar un toque ha entrado en contacto con la
pantalla en una ubicación particular.
onTapUpUn puntero que activa un toque ha dejado de estar en contacto con la
pantalla en una ubicación particular.
onTapHa ocurrido un toque.
onTapCancelEl puntero que anteriormente activó el
onTapDown
no causará un toque.
-
Double tapping
onDoubleTapEl usuario tocó la pantalla en la misma ubicación dos veces en
rápida sucesión.
-
Long pressing
onLongPressUn puntero ha permanecido en contacto con la pantalla
en la misma ubicación durante un período prolongado.
-
Vertical dragging
onVerticalDragStartUn puntero ha entrado en contacto con la pantalla y podría comenzar a
moverse verticalmente.
onVerticalDragUpdateUn puntero en contacto con la pantalla
se ha movido más en la dirección vertical.
onVerticalDragEndUn puntero que estaba previamente en contacto con la
pantalla y moviéndose verticalmente ya no está en contacto con la pantalla y se estaba moviendo a una velocidad específica cuando dejó de hacer contacto con la pantalla.
-
Horizontal dragging
onHorizontalDragStartUn puntero ha entrado en contacto con la pantalla y podría comenzar
a moverse horizontalmente.
onHorizontalDragUpdateUn puntero en contacto con la pantalla
se ha movido más en la dirección horizontal.
onHorizontalDragEndUn puntero que estaba previamente en contacto con la
pantalla y moviéndose horizontalmente ya no está en contacto con la pantalla.
El siguiente ejemplo muestra un GestureDetector
que rota el logotipo de Flutter al hacer doble toque:
class SampleApp extends StatefulWidget {
const SampleApp({super.key});
@override
State<SampleApp> createState() => _SampleAppState();
}
class _SampleAppState extends State<SampleApp>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
child: RotationTransition(
turns: curve,
child: const FlutterLogo(size: 200),
),
),
),
);
}
}
Temas, estilos y multimedia
#Las aplicaciones de Flutter son fáciles de diseñar; puedes cambiar entre temas claro y oscuro, cambiar el estilo de tu texto y componentes de UI, y más. Esta sección cubre aspectos del diseño de tus aplicaciones de Flutter y compara cómo podrías hacer lo mismo en UIKit.
Usar un tema
#De fábrica, Flutter viene con una hermosa implementación de Material Design, que se encarga de gran parte de las necesidades de diseño y personalización de temas que normalmente realizarías.
Para aprovechar al máximo los Material Components en tu aplicación,
declara un widget de nivel superior, MaterialApp,
como el punto de entrada a tu aplicación.
MaterialApp es un widget de conveniencia que envuelve una serie
de widgets que normalmente se requieren para las aplicaciones
que implementan Material Design.
Se basa en un WidgetsApp agregando funcionalidad específica de Material.
Pero Flutter es lo suficientemente flexible y expresivo para implementar cualquier lenguaje de diseño. En iOS, puedes usar la biblioteca Cupertino para producir una interfaz que se adhiera a las Directrices de interfaz humana (Human Interface Guidelines). Para obtener el conjunto completo de estos widgets, consulta la galería de widgets de Cupertino.
También puedes usar un WidgetsApp como el widget de tu aplicación,
el cual proporciona algunas de las mismas funcionalidades,
pero no es tan rico como MaterialApp.
Para personalizar los colores y estilos de cualquier componente hijo,
pasa un objeto ThemeData al widget MaterialApp.
Por ejemplo, en el código siguiente,
el esquema de color a partir de seed se establece en deepPurple y el color del divisor es gris.
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
dividerColor: Colors.grey,
),
home: const SampleAppPage(),
);
}
}
Usar fuentes personalizadas
#En UIKit, importas cualquier archivo de fuente ttf a tu proyecto
y creas una referencia en el archivo info.plist.
En Flutter, colocas el archivo de fuente en una carpeta
y haces referencia a él en el archivo pubspec.yaml,
de manera similar a cómo importas imágenes.
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
Luego asignas la fuente a tu widget Text:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: const Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
Estilizar texto
#Junto con las fuentes, puedes personalizar otros elementos de estilo en un widget Text.
El parámetro style de un widget Text recibe un objeto TextStyle,
donde puedes personalizar muchos parámetros, tales como:
colordecorationdecorationColordecorationStylefontFamilyfontSizefontStylefontWeighthashCodeheightinheritletterSpacingtextBaselinewordSpacing
Empaquetar imágenes en aplicaciones
#Aunque iOS trata las imágenes y los assets como elementos distintos,
las aplicaciones de Flutter solo tienen assets. Los recursos que se
colocan en la carpeta Images.xcasset en iOS,
se colocan en una carpeta de assets para Flutter.
Al igual que con iOS, los assets son cualquier tipo de archivo, no solo imágenes.
Por ejemplo, podrías tener un archivo JSON ubicado en la carpeta my-assets:
my-assets/data.json
Declara el asset en el archivo pubspec.yaml:
assets:
- my-assets/data.json
Y luego accede a él desde el código usando un AssetBundle:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}
Para las imágenes, Flutter sigue un formato simple basado en la densidad al igual que iOS.
Los assets de imagen pueden ser 1.0x, 2.0x, 3.0x, o cualquier otro multiplicador.
El devicePixelRatio de Flutter expresa la relación
de píxeles físicos en un único píxel lógico.
Los assets se encuentran en cualquier carpeta arbitraria;
Flutter no tiene una estructura de carpetas predefinida.
Declaras los assets (con su ubicación) en
el archivo pubspec.yaml, y Flutter los detecta.
Por ejemplo, para agregar una imagen llamada my_icon.png a tu proyecto de Flutter,
podrías decidir almacenarla en una carpeta llamada arbitrariamente images.
Coloca la imagen base (1.0x) en la carpeta images, y las
otras variantes en subcarpetas con el nombre del multiplicador de relación correspondiente:
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
A continuación, declara estas imágenes en el archivo pubspec.yaml:
assets:
- images/my_icon.png
Ahora puedes acceder a tus imágenes usando AssetImage:
image: AssetImage('images/a_dot_burr.png'),
o directamente en un widget Image:
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
Para obtener más detalles, consulta Agregar assets e imágenes en Flutter.
Entrada de formulario
#Esta sección analiza cómo usar formularios en Flutter y cómo se comparan con UIKit.
Recuperar la entrada del usuario
#Dado cómo Flutter utiliza widgets inmutables con un State separado, es posible que te preguntes cómo encaja la entrada del usuario en la ecuación. En UIKit, normalmente consultas los widgets por sus valores actuales cuando llega el momento de enviar la entrada del usuario o actuar sobre ella. ¿Cómo funciona eso en Flutter?
En la práctica, los formularios se manejan, como todo en Flutter,
mediante widgets especializados. Si tienes un TextField o un
TextFormField, puedes proporcionar un TextEditingController
para recuperar la entrada del usuario:
class _MyFormState extends State<MyForm> {
// Create a text controller and use it to retrieve the current value.
// of the TextField!
final myController = TextEditingController();
@override
void dispose() {
// Clean up the controller when disposing of the Widget.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Retrieve Text Input')),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(controller: myController),
),
floatingActionButton: FloatingActionButton(
// When the user presses the button, show an alert dialog with the
// text the user has typed into our text field.
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the user has typed in using our
// TextEditingController.
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: const Icon(Icons.text_fields),
),
);
}
}
Puedes encontrar más información y la lista de código completa en Recuperar el valor de un campo de texto.
Marcador de posición en un campo de texto
#En Flutter, puedes mostrar fácilmente un "hint" (sugerencia) o un texto de marcador de posición
para tu campo agregando un objeto InputDecoration
al parámetro de constructor decoration del widget Text:
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)
Mostrar errores de validación
#Al igual que harías con un "hint", pasa un objeto InputDecoration
al constructor de decoración para el widget Text.
Sin embargo, no querrás empezar mostrando un error.
En su lugar, cuando el usuario haya introducido datos no válidos,
actualiza el state y pasa un nuevo objeto InputDecoration.
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String? _errorText;
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: TextField(
onSubmitted: (text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(
hintText: 'This is a hint',
errorText: _errorText,
),
),
),
);
}
}
Subprocesos y asincronía
#Esta sección analiza la concurrencia en Flutter y cómo se compara con UIKit.
Escribir código asíncrono
#Dart tiene un modelo de ejecución de un solo hilo,
con soporte para Isolates
(una forma de ejecutar código Dart en otro hilo),
un bucle de eventos (event loop) y programación asíncrona.
A menos que generes un Isolate,
tu código Dart se ejecuta en el hilo principal de la UI y es
impulsado por un bucle de eventos. El bucle de eventos de Flutter es
equivalente al bucle principal de iOS, es decir,
el Looper que está adjunto al hilo principal.
El modelo de un solo hilo de Dart no significa que debas
ejecutar todo como una operación bloqueante
que congele la UI. En su lugar,
utiliza las facilidades asíncronas que proporciona el lenguaje Dart,
como async/await, para realizar trabajo asíncrono.
Por ejemplo, puedes ejecutar código de red sin que la
UI se cuelgue usando async/await y dejando que Dart haga
el trabajo pesado:
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Una vez completada la llamada de red con await,
actualiza la UI llamando a setState(),
lo que desencadena una reconstrucción del subárbol de widgets
y actualiza los datos.
El siguiente ejemplo carga datos de forma asíncrona y
los muestra en un ListView:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Widget getRow(int index) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text('Row ${data[index]['title']}'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
),
);
}
}
Consulta la siguiente sección para obtener más información sobre cómo realizar trabajo en segundo plano y en qué se diferencia Flutter de iOS.
Mover al hilo de fondo
#Dado que Flutter es monohilo y ejecuta un bucle de eventos
(como Node.js), no tienes que preocuparte por la
gestión de hilos ni por generar hilos en segundo plano.
Si estás realizando trabajo ligado a I/O,
como el acceso al disco o una llamada de red,
entonces puedes usar con seguridad async/await y listo.
Si, por otro lado, necesitas realizar un trabajo computacionalmente intensivo
que mantenga ocupada la CPU, querrás moverlo a un
Isolate para evitar bloquear el bucle de eventos.
Para el trabajo de entrada/salida, declara la función como una función async,
y haz await en las tareas de larga duración dentro de la función:
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Así es como normalmente realizas llamadas de red o de base de datos, las cuales son ambas operaciones de I/O.
Sin embargo, hay ocasiones en las que podrías estar procesando
una gran cantidad de datos y tu UI se congela.
En Flutter, usa Isolates para aprovechar
múltiples núcleos de CPU para realizar tareas de larga duración o
computacionalmente intensivas.
Los Isolates son hilos de ejecución separados que no comparten
ninguna memoria con el montón (heap) de memoria de ejecución principal.
Esto significa que no puedes acceder a variables desde el hilo principal,
ni actualizar tu UI llamando a setState().
Los Isolates hacen honor a su nombre, y no pueden compartir memoria
(en forma de campos estáticos, por ejemplo).
El siguiente ejemplo muestra, en un isolate simple, cómo compartir datos de vuelta al hilo principal para actualizar la UI.
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Aquí, dataLoader() es el Isolate que se ejecuta en
su propio hilo de ejecución separado.
En el isolate, puedes realizar un procesamiento más intensivo de la CPU
(analizar un JSON grande, por ejemplo),
o realizar operaciones matemáticas computacionalmente intensivas,
como cifrado o procesamiento de señales.
Puedes ejecutar el ejemplo completo a continuación:
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Widget getBody() {
bool showLoadingDialog = data.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
}
Realizar solicitudes de red
#Realizar una llamada de red en Flutter es fácil cuando
utilizas el popular paquete http. Esto abstrae
gran parte del trabajo de red que normalmente implementarías tú mismo,
facilitando la realización de llamadas de red.
Para agregar el paquete http como una dependencia, ejecuta flutter pub add:
flutter pub add http
Para realizar una llamada de red,
llama a await en la función async http.get():
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Mostrar el progreso en tareas de larga duración
#En UIKit, normalmente usas un UIProgressView
mientras ejecutas una tarea de larga duración en segundo plano.
En Flutter, usa un widget ProgressIndicator.
Muestra el progreso mediante programación controlando
cuándo se renderiza a través de una bandera booleana.
Indícale a Flutter que actualice su State antes de que comience tu tarea de larga duración,
y ocúltalo después de que termine.
En el ejemplo siguiente, la función build se separa en tres
funciones diferentes. Si showLoadingDialog es true
(cuando widgets.length == 0), entonces renderiza el ProgressIndicator.
De lo contrario, renderiza el ListView con los datos devueltos por una llamada de red.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Widget getBody() {
if (showLoadingDialog) {
return getProgressDialog();
}
return getListView();
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
}
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.