Saltar al contenido principal

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.

Ver en YouTube en una pestaña nueva: "Flutter para desarrolladores 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.

dart
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:

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: '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:

dart
@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:

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: '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:

dart
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.

dart
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:

dart
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:

dart
@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.

#

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.

#

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 Map de nombres de rutas.
  • Navegar directamente a una ruta.

El siguiente ejemplo compila un Map.

dart
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.

dart
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:

dart
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:

dart
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
#

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:

objc
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:

dart
class Strings {
  static const String welcomeMessage = 'Welcome To Flutter';
}

Puedes acceder a tus strings de esta manera:

dart
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.

yaml
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:

dart
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:

inactive

La 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.

paused

La aplicación no está visible actualmente para el usuario,

no responde a la entrada del usuario, pero se está ejecutando en segundo plano.

resumed

La aplicación está visible y responde a la entrada del usuario.

suspending

La 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.

dart
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.

dart
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.

dart
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.

dart
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.

dart
@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:

  1. 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 ElevatedButton tiene un parámetro onPressed:
dart
@override
Widget build(BuildContext context) {
 return ElevatedButton(
   onPressed: () {
     developer.log('click');
   },
   child: const Text('Button'),
 );
}
  1. 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.
dart
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

    onTapDown

    Un puntero que podría causar un toque ha entrado en contacto con la

    pantalla en una ubicación particular.

    onTapUp

    Un puntero que activa un toque ha dejado de estar en contacto con la

    pantalla en una ubicación particular.

    onTap

    Ha ocurrido un toque.

    onTapCancel

    El puntero que anteriormente activó el onTapDown

    no causará un toque.

  • Double tapping

    onDoubleTap

    El usuario tocó la pantalla en la misma ubicación dos veces en

    rápida sucesión.

  • Long pressing

    onLongPress

    Un puntero ha permanecido en contacto con la pantalla

    en la misma ubicación durante un período prolongado.

  • Vertical dragging

    onVerticalDragStart

    Un puntero ha entrado en contacto con la pantalla y podría comenzar a

    moverse verticalmente.

    onVerticalDragUpdate

    Un puntero en contacto con la pantalla

    se ha movido más en la dirección vertical.

    onVerticalDragEnd

    Un 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

    onHorizontalDragStart

    Un puntero ha entrado en contacto con la pantalla y podría comenzar

    a moverse horizontalmente.

    onHorizontalDragUpdate

    Un puntero en contacto con la pantalla

    se ha movido más en la dirección horizontal.

    onHorizontalDragEnd

    Un 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:

dart
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.

dart
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.

yaml
fonts:
  - family: MyCustomFont
    fonts:
      - asset: fonts/MyCustomFont.ttf
      - style: italic

Luego asignas la fuente a tu widget Text:

dart
@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:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

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:

yaml
assets:
 - my-assets/data.json

Y luego accede a él desde el código usando un AssetBundle:

dart
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:

yaml
assets:
 - images/my_icon.png

Ahora puedes acceder a tus imágenes usando AssetImage:

dart
image: AssetImage('images/a_dot_burr.png'),

o directamente en un widget Image:

dart
@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:

dart
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:

dart
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.

dart
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:

dart
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:

dart
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:

dart
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.

dart
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:

dart
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():

dart
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.

dart
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(),
    );
  }
}