Saltar al contenido principal

Flutter para desarrolladores de Xamarin.Forms

Aprende cómo aplicar tus conocimientos de desarrollo de Xamarin.Forms al crear aplicaciones con Flutter.

Este documento está destinado a desarrolladores de Xamarin.Forms que buscan aplicar sus conocimientos existentes para crear aplicaciones móviles con Flutter. Si comprendes los fundamentos del framework de Xamarin.Forms, entonces puedes usar este documento como un punto de partida rápido para el desarrollo en Flutter.

Tus conocimientos y habilidades en Android e iOS son valiosos al compilar con Flutter, porque Flutter depende de las configuraciones del sistema operativo nativo, de manera similar a cómo configurarías tus proyectos nativos de Xamarin.Forms. El Framework de Flutter también es similar en la forma en que creas una única UI que se utiliza en múltiples plataformas.

Este documento se puede utilizar como un libro de recetas, saltando y encontrando las preguntas que sean más relevantes para tus necesidades.

Configuración del proyecto

#

¿Cómo se inicia la aplicación?

#

Para cada plataforma en Xamarin.Forms, llamas al método LoadApplication, que crea una nueva aplicación e inicia tu app.

csharp
LoadApplication(new App());

En Flutter, el punto de entrada principal predeterminado es main donde cargas tu aplicación Flutter.

dart
void main() {
  runApp(const MyApp());
}

En Xamarin.Forms, asignas una Page a la propiedad MainPage en la clase Application.

csharp
public class App : Application
{
    public App()
    {
        MainPage = new ContentPage
        {
            Content = new Label
            {
                Text = "Hello World",
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center
            }
        };
    }
}

En Flutter, "todo es un widget", incluso la propia aplicación. El siguiente ejemplo muestra MyApp, un simple Widget de aplicación.

dart
class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Hello World!', textDirection: TextDirection.ltr),
    );
  }
}

¿Cómo se crea una página?

#

Xamarin.Forms tiene muchos tipos de páginas; ContentPage es la más común. En Flutter, especificas un widget de aplicación que contiene tu página raíz. Puedes usar un widget MaterialApp, que es compatible con Material Design, o puedes usar un widget CupertinoApp, que es compatible con una aplicación al estilo de iOS, o puedes usar el nivel más bajo WidgetsApp, que puedes personalizar de la manera que desees.

El siguiente código define la página de inicio, un widget stateful. En Flutter, todos los widgets son inmutables, pero se admiten dos tipos de widgets: Stateful y Stateless. Ejemplos de un widget stateless son títulos, iconos o imágenes.

El siguiente ejemplo utiliza MaterialApp, que contiene su página raíz en la propiedad home.

dart
class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

Desde aquí, tu primera página real es otro Widget, en el que creas tu State.

Un widget Stateful, como MyHomePage a continuación, consta de dos partes. La primera parte, que en sí misma es inmutable, crea un objeto State que contiene el State del objeto. El objeto State persiste durante la vida del widget.

dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

El objeto State implementa el método build() para el widget stateful.

Cuando cambia el State del árbol de widgets, llama a setState(), lo que desencadena una compilación de esa parte de la UI. Asegúrate de llamar a setState() solo cuando sea necesario, y solo en la parte del árbol de widgets que haya cambiado, o de lo contrario puede resultar en un rendimiento deficiente de la UI.

dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set the appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

En Flutter, la UI (también conocida como árbol de widgets), es inmutable, lo que significa que no puedes cambiar su State una vez que se ha construido. Cambias los campos en tu clase State, luego llamas a setState() para reconstruir todo el árbol de widgets de nuevo.

Esta forma de generar la UI es diferente a la de Xamarin.Forms, pero este enfoque tiene muchos beneficios.

Vistas

#

¿Cuál es el equivalente de una Page o Element en Flutter?

#

ContentPage, TabbedPage, FlyoutPage son todos tipos de páginas que podrías usar en una aplicación de Xamarin.Forms. Estas páginas luego contendrían Elements para mostrar los diversos controles. En Xamarin.Forms, un Entry o un Button son ejemplos de un Element.

En Flutter, casi todo es un widget. Una Page, llamada Route en Flutter, es un widget. Los botones, las barras de progreso y los controladores de animación son todos widgets. Al construir una ruta, creas un árbol de widgets.

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. Por ejemplo, en iOS, puedes usar los widgets de Cupertino para producir una interfaz que se parezca al lenguaje de diseño iOS de Apple.

¿Cómo actualizo los widgets?

#

En Xamarin.Forms, cada Page o Element es una clase stateful, que tiene propiedades y métodos. Actualizas tu Element actualizando una propiedad, y esto se propaga al control nativo.

En Flutter, los Widgets son inmutables y no puedes actualizarlos directamente cambiando una propiedad, en su lugar tienes que trabajar con el State del widget.

De aquí proviene el concepto de widgets Stateful vs Stateless. Un StatelessWidget es exactamente lo que parece: un widget sin información de State.

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 en el objeto.

Por ejemplo, en Xamarin.Forms, esto es similar a colocar un Image con tu logotipo. El logotipo no va a cambiar durante el tiempo de ejecución, por lo que usas un StatelessWidget en Flutter.

Si deseas cambiar dinámicamente la UI según los datos recibidos después de realizar una llamada HTTP o una interacción del usuario, entonces tienes que trabajar con StatefulWidget e indicarle al framework de Flutter que el State del widget se ha actualizado, para que pueda actualizar ese widget.

Lo importante a tener en cuenta aquí es que, en esencia, tanto los widgets stateless como los stateful se comportan igual. Se reconstruyen en cada fotograma, la diferencia es que el StatefulWidget tiene un objeto State que almacena datos de State a lo largo de los fotogramas y los restaura.

Si tienes dudas, recuerda siempre esta regla: si un widget cambia (debido a las interacciones del usuario, por ejemplo), es stateful. Sin embargo, si un widget reacciona al cambio, el widget padre contenedor aún puede ser stateless si este no reacciona al cambio por sí mismo.

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
const Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

Como puedes ver, el widget Text no tiene información de State asociada, renderiza lo que se pasa en sus constructores y nada más.

But, ¿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, como se muestra en el siguiente ejemplo:

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

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

¿Cómo distribuyo mis widgets? ¿Cuál es el equivalente de un archivo XAML?

#

En Xamarin.Forms, la mayoría de los desarrolladores escriben layouts en XAML, aunque a veces en C#. En Flutter, escribes tus layouts con un árbol de widgets en el código.

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: ElevatedButton(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.only(left: 20, right: 30),
        ),
        onPressed: () {},
        child: const Text('Hello'),
      ),
    ),
  );
}

Puedes ver los layouts que ofrece Flutter en el catálogo de widgets.

¿Cómo agrego o elimino un Element de mi layout?

#

En Xamarin.Forms, tenías que eliminar o agregar un Element en el código. Esto implicaba establecer la propiedad Content o llamar a Add() o Remove() si era una lista.

En Flutter, debido a que los widgets son inmutables, no existe un equivalente directo. 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),
      ),
    );
  }
}

¿Cómo animo un widget?

#

En Xamarin.Forms, creas animaciones simples utilizando ViewExtensions que incluyen métodos como FadeTo y TranslateTo. Usarías estos métodos en una vista para realizar las animaciones requeridas.

xml
<Image Source="{Binding MyImage}" x:Name="myImage" />

Luego, en el código subyacente (code-behind), o en un behavior, esto desvanecería la imagen en un período de 1 segundo.

csharp
myImage.FadeTo(0, 1000);

En Flutter, animas los widgets usando la biblioteca de animación al envolver los widgets dentro de un widget animado. Usa un AnimationController, que es un Animation<double> que puede pausar, buscar, detener y revertir la animación. Requiere un Ticker que indique cuándo ocurre vsync, y produce una interpolación lineal entre 0 y 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 logotipo cuando presionas el FloatingActionButton:

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  /// This widget is the root of your application.
  const FadeAppTest({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 TickerProviderStateMixin {
  late final AnimationController controller;
  late final CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

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

¿Cómo dibujo/pinto en la pantalla?

#

Xamarin.Forms nunca tuvo una forma integrada de dibujar directamente en la pantalla. Muchos usarían SkiaSharp, si necesitaran dibujar una imagen personalizada. En Flutter, tienes acceso directo al Skia Canvas y puedes dibujar fácilmente en la pantalla.

Flutter tiene dos clases que te ayudan a dibujar en el lienzo (canvas): CustomPaint y CustomPainter, siendo esta última la que implementa tu algoritmo para dibujar en el lienzo.

Para aprender a implementar un signature painter en Flutter, consulta la respuesta de Collin sobre Custom Paint.

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
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset?>[];

  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      final RenderBox referenceBox = context.findRenderObject() as RenderBox;
      final Offset localPosition = referenceBox.globalToLocal(
        details.globalPosition,
      );
      _points = List.from(_points)..add(localPosition);
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _onPanUpdate,
      onPanEnd: (details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  const 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;
}

¿Dónde está la opacidad del widget?

#

En Xamarin.Forms, todos los VisualElements tienen una Opacity. En Flutter, necesitas envolver un widget en un widget Opacity para lograr esto.

¿Cómo construyo widgets personalizados?

#

En Xamarin.Forms, normalmente creas una subclase de VisualElement, o usas un VisualElement 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). Es algo similar a implementar un control personalizado basado en un Grid con numerosos VisualElements agregados, mientras se extiende con lógica personalizada.

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'));
}
#

¿Cómo navego entre páginas?

#

En Xamarin.Forms, la clase NavigationPage class proporciona una experiencia de navegación jerárquica en la que el usuario puede navegar por las páginas, hacia adelante y hacia atrás.

Flutter tiene una implementación similar, utilizando un Navigator y Routes. Una Route es una abstracción para una Page de una aplicación, y un Navigator es un widget que gestiona rutas.

Una ruta se mapea aproximadamente a una Page. El navigator funciona de manera similar a la NavigationPage de Xamarin.Forms, en el sentido de que puede realizar push() y pop() de 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. (MaterialApp)
  • Navegar directamente a una ruta. (WidgetsApp)

El siguiente ejemplo construye un Map.

dart
void main() {
  runApp(
    MaterialApp(
      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 enviando su nombre al Navigator.

dart
Navigator.of(context).pushNamed('/b');

El Navigator es una pila que gestiona las rutas de tu aplicación. Enviar (push) una ruta a la pila te lleva a esa ruta. Retirar (pop) una ruta de la pila te devuelve a la ruta anterior. Esto se hace esperando (awaiting) la Future devuelta por push().

async/await es muy similar a la implementación de .NET y se explica con más detalle en UI asíncrona.

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});

¿Cómo navego a otra aplicación?

#

En Xamarin.Forms, para enviar al usuario a otra aplicación, utilizas un esquema de URI específico, usando Device.OpenUrl("mailto://").

Para implementar esta funcionalidad en Flutter, crea una integración de plataforma nativa, o usa un plugin existente, como url_launcher, disponible junto con muchos otros paquetes en pub.dev.

UI asíncrona

#

¿Cuál es el equivalente de Device.BeginOnMainThread() en Flutter?

#

Dart tiene un modelo de ejecución monohilo, con soporte para Isolates (una forma de ejecutar códigos 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 modelo monohilo de Dart no significa que necesites ejecutar todo como una operación bloqueante que haga que la UI se congele. Al igual que en Xamarin.Forms, debes mantener libre el hilo de la UI. Usarías async/await para realizar tareas en las que debes esperar la respuesta.

En Flutter, usa las facilidades asíncronas que proporciona el lenguaje Dart, también llamadas async/await, para realizar trabajo asíncrono. Esto es muy similar a C# y debería ser muy fácil de usar para cualquier desarrollador de Xamarin.Forms.

Por ejemplo, puedes ejecutar código de red sin causar 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 esperada, update 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 cómo difiere Flutter de Android.

¿Cómo se mueve el trabajo a un hilo de fondo?

#

Dado que Flutter es monohilo y ejecuta un bucle de eventos, no tienes que preocuparte por la gestión de hilos ni por generar hilos en segundo plano. Esto es muy similar a Xamarin.Forms. 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 ya está.

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 (event loop), del mismo modo que mantendrías cualquier tipo de trabajo fuera del hilo principal. Esto es similar a cuando mueves cosas a un hilo diferente mediante Task.Run() en Xamarin.Forms.

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 realizarías 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 los múltiples núcleos de la 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. Esta es una diferencia con respecto a Task.Run(). Esto significa que no puedes acceder a variables desde el hilo principal, ni actualizar tu UI llamando a setState().

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() {
    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 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: getBody(),
    );
  }
}

¿Cómo realizo solicitudes de red?

#

En Xamarin.Forms usarías HttpClient. 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 usar el paquete http, agrégalo a tus dependencias en pubspec.yaml:

yaml
dependencies:
  http: ^1.4.0

Para realizar una solicitud 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?>>();
  });
}

¿Cómo muestro el progreso de una tarea de larga duración?

#

En Xamarin.Forms normalmente crearías un indicador de carga, ya sea directamente en XAML o a través de un plugin de terceros como AcrDialogs.

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:async';
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 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: getBody(),
    );
  }
}

Estructura del proyecto y recursos

#

¿Dónde almaceno mis archivos de imagen?

#

Xamarin.Forms no tiene una forma independiente de la plataforma para almacenar imágenes, tenías que colocar las imágenes en la carpeta xcasset de iOS, o en Android en las diversas carpetas drawable.

Aunque Android e iOS tratan los recursos y los assets como elementos distintos, las aplicaciones de Flutter solo tienen assets. Todos los recursos que vivirían en las carpetas Resources/drawable-* en Android, se colocan en una carpeta de assets para Flutter.

Flutter sigue un formato simple basado en la densidad al igual que iOS. Los assets pueden ser de 1.0x, 2.0x, 3.0x o cualquier otro multiplicador. Flutter no tiene dps, sino que hay píxeles lógicos, que son básicamente lo mismo que los píxeles independientes del dispositivo. El devicePixelRatio de Flutter expresa la relación de píxeles físicos en un solo píxel lógico.

El equivalente a las categorías de densidad de Android son:

Calificador de densidad de AndroidRelación de píxeles de Flutter
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

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.

Para agregar un nuevo asset de imagen llamado my_icon.png a nuestro proyecto Flutter, por ejemplo, y decidir que debe vivir en una carpeta que llamamos arbitrariamente images, colocarías la imagen base (1.0x) en la carpeta images, y todas las demás variantes en subcarpetas con el multiplicador de relación apropiado:

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, deberás declarar estas imágenes en tu archivo pubspec.yaml:

yaml
assets:
 - images/my_icon.png

Puedes acceder directamente a tus imágenes en un widget Image.asset:

dart
@override
Widget build(BuildContext context) {
  return Image.asset('images/my_icon.png');
}

o utilizando AssetImage:

dart
@override
Widget build(BuildContext context) {
  return const Image(image: AssetImage('images/my_image.png'));
}

Se puede encontrar información más detallada en Agregar assets e imágenes.

¿Dónde almaceno las cadenas de texto? ¿Cómo manejo la localización?

#

A diferencia de .NET que tiene archivos resx, 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_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.

¿Dónde está mi archivo de proyecto?

#

En Xamarin.Forms tendrás un archivo csproj. El equivalente más cercano en Flutter es pubspec.yaml, que contiene las dependencias de paquetes y varios detalles del proyecto. Similar a .NET Standard, los archivos dentro del mismo directorio se consideran parte del proyecto.

¿Cuál es el equivalente de NuGet? ¿Cómo agrego dependencias?

#

En el ecosistema .NET, los proyectos nativos de Xamarin y los proyectos de Xamarin.Forms tenían acceso a NuGet y al sistema de gestión de paquetes integrado. Las aplicaciones de Flutter contienen una aplicación nativa de Android, una aplicación nativa de iOS y la aplicación Flutter.

En Android, agregas dependencias añadiéndolas a tu script de compilación de Gradle. En iOS, agregas dependencias añadiéndolas a tu Podfile.

Flutter utiliza el propio sistema de compilación de Dart y el gestor de paquetes Pub. Las herramientas delegan la compilación de las aplicaciones contenedor de Android e iOS nativas a los respectivos sistemas de compilación.

En general, utiliza pubspec.yaml para declarar dependencias externas para usar en Flutter. Un buen lugar para encontrar paquetes de Flutter es en pub.dev.

Ciclo de vida de la aplicación

#

¿Cómo escucho los eventos del ciclo de vida de la aplicación?

#

En Xamarin.Forms, tienes una Application que contiene OnStart, OnResume y OnSleep. En Flutter, en su lugar puedes escuchar eventos de ciclo de vida similares 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 es solo para iOS.

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. Este evento es solo para Android.

Para obtener más detalles sobre el significado de estos estados, consulta la documentación de AppLifecycleStatus.

Diseños (Layouts)

#

¿Cuál es el equivalente de un StackLayout?

#

En Xamarin.Forms puedes crear un StackLayout con una Orientation horizontal o vertical. Flutter tiene un enfoque similar, sin embargo, usarías los widgets Row o Column.

Si te fijas, los dos ejemplos de código son idénticos excepto por el widget Row y Column. Los hijos son los mismos y esta característica se puede aprovechar para desarrollar layouts ricos que pueden cambiar con el tiempo con los mismos hijos.

dart
@override
Widget build(BuildContext context) {
  return const Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
dart
@override
Widget build(BuildContext context) {
  return const Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );

¿Cuál es el equivalente de un Grid?

#

El equivalente más cercano a un Grid sería un GridView. Esto es mucho más potente de lo que estás acostumbrado en Xamarin.Forms. Un GridView proporciona desplazamiento automático cuando el contenido excede su espacio visible.

dart
@override
Widget build(BuildContext context) {
  return GridView.count(
    // Create a grid with 2 columns. If you change the scrollDirection to
    // horizontal, this would produce 2 rows.
    crossAxisCount: 2,
    // Generate 100 widgets that display their index in the list.
    children: List<Widget>.generate(100, (index) {
      return Center(
        child: Text(
          'Item $index',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      );
    }),
  );
}

Es posible que hayas utilizado un Grid en Xamarin.Forms para implementar widgets que se superponen a otros widgets. En Flutter, logras esto con el widget Stack.

Este ejemplo crea dos iconos que se superponen entre sí.

dart
@override
Widget build(BuildContext context) {
  return const Stack(
    children: <Widget>[
      Icon(Icons.add_box, size: 24, color: Colors.black),
      Positioned(
        left: 10,
        child: Icon(Icons.add_circle, size: 24, color: Colors.black),
      ),
    ],
  );
}

¿Cuál es el equivalente de un ScrollView?

#

En Xamarin.Forms, un ScrollView envuelve un VisualElement, y si el contenido es más grande que la pantalla del dispositivo, se desplaza.

En Flutter, la coincidencia más cercana es el widget SingleChildScrollView. Simplemente llenas el Widget con el contenido que deseas que sea desplazable.

dart
@override
Widget build(BuildContext context) {
  return const SingleChildScrollView(child: Text('Long Content'));
}

Si tienes muchos elementos que deseas envolver en un scroll, incluso de diferentes tipos de Widget, es posible que desees usar un ListView. Esto podría parecer exagerado, pero en Flutter está mucho más optimizado y es menos intensivo que un ListView de Xamarin.Forms, que se basa en controles específicos de la plataforma.

dart
@override
Widget build(BuildContext context) {
  return ListView(
    children: const <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

¿Cómo manejo las transiciones a horizontal (landscape) en Flutter?

#

Las transiciones a modo horizontal (landscape) se pueden manejar automáticamente configurando la propiedad configChanges en el AndroidManifest.xml:

xml
<activity android:configChanges="orientation|screenSize" />

Detección de gestos y manejo de eventos táctiles

#

¿Cómo agrego GestureRecognizers a un widget en Flutter?

#

En Xamarin.Forms, los Elements pueden contener un evento de clic al que te puedes vincular. Muchos elementos también contienen un Command que está asociado a este evento. Alternativamente, usarías el TapGestureRecognizer. En Flutter hay dos formas muy similares:

  1. Si el widget admite la detección de eventos, pásale una función y manéjala en la función. Por ejemplo, el ElevatedButton tiene un parámetro onPressed:

    dart
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
    
  2. 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 SampleApp extends StatelessWidget {
      const SampleApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: GestureDetector(
              onTap: () {
                developer.log('tap');
              },
              child: const FlutterLogo(size: 200),
            ),
          ),
        );
      }
    }
    

¿Cómo manejo otros gestos en los widgets?

#

En Xamarin.Forms agregarías un GestureRecognizer a la View. Normalmente estarías limitado a TapGestureRecognizer, PinchGestureRecognizer, PanGestureRecognizer, SwipeGestureRecognizer, DragGestureRecognizer y DropGestureRecognizer a menos que construyeras el tuyo propio.

En Flutter, utilizando el GestureDetector, puedes escuchar una amplia gama de gestos como:

  • Tap
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.

  • Doble tap
onDoubleTap

El usuario tocó la pantalla en la misma ubicación dos veces en rápida sucesión.

  • Presión prolongada
onLongPress

Un puntero ha permanecido en contacto con la pantalla en la misma ubicación durante un período prolongado.

  • Arrastre vertical
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.

  • Arrastre horizontal
onHorizontalDragStart

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

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 y se estaba moviendo a una velocidad específica cuando dejó de hacer contacto con la pantalla.

El siguiente ejemplo muestra un GestureDetector que rota el logotipo de Flutter al hacer doble toque:

dart
class RotatingFlutterDetector extends StatefulWidget {
  const RotatingFlutterDetector({super.key});

  @override
  State<RotatingFlutterDetector> createState() =>
      _RotatingFlutterDetectorState();
}

class _RotatingFlutterDetectorState extends State<RotatingFlutterDetector>
    with SingleTickerProviderStateMixin {
  late final AnimationController controller;
  late final CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    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),
          ),
        ),
      ),
    );
  }
}

ListViews y adaptadores

#

¿Cuál es el equivalente a un ListView en Flutter?

#

El equivalente a un ListView en Flutter es … ¡un ListView!

En un ListView de Xamarin.Forms, creas una ViewCell y posiblemente un DataTemplateSelector y lo pasas al ListView, el cual renderiza cada fila con lo que devuelve tu DataTemplateSelector o ViewCell. Sin embargo, a menudo debes asegurarte de activar el reciclaje de celdas (Cell Recycling) de lo contrario tendrás problemas de memoria y velocidades de desplazamiento lentas.

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 sea rápido y fluido.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

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 StatelessWidget {
  const SampleAppPage({super.key});

  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) =>
          Padding(padding: const EdgeInsets.all(10), child: Text('Row $index')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

¿Cómo sé qué elemento de la lista se ha hecho clic?

#

En Xamarin.Forms, el ListView tiene un método ItemTapped para averiguar qué elemento se hizo clic. Hay muchas otras técnicas que podrías haber utilizado como verificar cuándo cambian los comportamientos de SelectedItem o EventToCommand.

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 {
  // 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> {
  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => GestureDetector(
        onTap: () {
          developer.log('Row $index tapped');
        },
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Text('Row $index'),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

¿Cómo actualizo un ListView dinámicamente?

#

En Xamarin.Forms, si vinculaste la propiedad ItemsSource a una ObservableCollection, simplemente actualizarías la lista en tu ViewModel. Alternativamente, podrías asignar una nueva List a la propiedad ItemsSource.

En Flutter, las cosas funcionan un poco diferente. Si actualizas la lista de widgets dentro de un método setState(), verías rápidamente que tus datos no cambiaron 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 grandes conjuntos de datos, 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 {
  /// 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> {
  List<Widget> widgets = <Widget>[];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  Widget getRow(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets = List<Widget>.from(widgets);
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @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. Esto es básicamente el equivalente de RecyclerView en Android, que automáticamente recicla los elementos de la lista por ti:

dart
import 'dart:developer' as developer;
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

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> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  Widget getRow(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (context, index) {
          return getRow(index);
        },
      ),
    );
  }
}

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 item builder.

La función item builder es similar a la función getView in un adaptador de Android; toma una posición, y devuelve la fila que deseas renderizar en esa posición.

Finalmente, pero más importante, ten en cuenta que la función onTap() ya no recrea la lista, sino que agrega elementos a ella.

Para más información, consulta el codelab Tu primera aplicación de Flutter.

Trabajar con texto

#

¿Cómo configuro fuentes personalizadas en mis widgets de texto?

#

En Xamarin.Forms, tendrías que agregar una fuente personalizada en cada proyecto nativo. Luego, en tu Element asignarías este nombre de fuente al atributo FontFamily usando nombre_archivo#nombre_fuente y solo nombre_fuente para iOS.

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

¿Cómo doy estilo a mis widgets de 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

Entrada de formulario

#

¿Cómo recupero la entrada del usuario?

#

Los elements de Xamarin.Forms te permiten consultar directamente el element para determinar el State de sus propiedades, o si está vinculado a una propiedad en un ViewModel.

La recuperación de información en Flutter es manejada por widgets especializados y es diferente a lo que estás acostumbrado. Si tienes un TextField o un TextFormField, puedes proporcionar un TextEditingController para recuperar la entrada del usuario:

dart
import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  const MyForm({super.key});

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  /// Create a text controller and use it to retrieve the current value
  /// of the TextField.
  final TextEditingController 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 that the user has typed into our text field.
        onPressed: () {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text that the user has entered using the
                // 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.

¿Cuál es el equivalente de un Placeholder en un Entry?

#

En Xamarin.Forms, algunos Elements admiten una propiedad Placeholder a la que puedes asignar un valor. Por ejemplo:

xml
<Entry Placeholder="This is a hint">

En Flutter, puedes mostrar fácilmente un "hint" (sugerencia) o un texto de marcador de posición para tu entrada agregando un objeto InputDecoration al parámetro de constructor decoration para el widget de texto.

dart
TextField(decoration: InputDecoration(hintText: 'This is a hint')),

¿Cómo muestro los errores de validación?

#

Con Xamarin.Forms, si desearas proporcionar una sugerencia visual de un error de validación, necesitarías crear nuevas propiedades y VisualElements que rodeen a los Elements que tenían errores de validación.

En Flutter, pasas un objeto InputDecoration al constructor de decoración para el widget de texto.

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 {
  /// 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> {
  String? _errorText;

  String? _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    const 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,}))$';
    final 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: _getErrorText(),
          ),
        ),
      ),
    );
  }
}

Plugins de Flutter

#

Interactuar con el hardware, servicios de terceros y la plataforma

#

¿Cómo interactúo con la plataforma y con el código nativo de la plataforma?

#

Flutter no ejecuta el código directamente en la plataforma subyacente; más bien, el código Dart que compone una aplicación de Flutter se ejecuta de forma nativa en el dispositivo, "esquivando" el SDK proporcionado por la plataforma. Eso significa, por ejemplo, que cuando realizas una solicitud de red en Dart, se ejecuta directamente en el contexto de Dart. No utilizas las APIs de Android o iOS de las que normalmente te beneficias al escribir aplicaciones nativas. Tu aplicación de Flutter todavía está alojada en un ViewController o Activity de una aplicación nativa como una vista, but no tienes acceso directo a esto, ni al framework nativo.

Esto no significa que las aplicaciones de Flutter no puedan interactuar con esas APIs nativas, o con cualquier código nativo que tengas. Flutter proporciona canales de plataforma (platform channels) que se comunican e intercambian datos con el ViewController o Activity que aloja tu vista de Flutter. Los canales de plataforma son esencialmente un mecanismo de mensajería asíncrona que conecta el código Dart con el ViewController o Activity anfitrión y el framework de iOS o Android en el que se ejecuta. Puedes usar canales de plataforma para ejecutar un método en el lado nativo, o para recuperar algunos datos de los sensores del dispositivo, por ejemplo.

Además de usar directamente canales de plataforma, puedes usar una variedad de plugins prediseñados que encapsulan el código nativo y de Dart para un objetivo específico. Por ejemplo, puedes usar un plugin para acceder al carrete de fotos (camera roll) y a la cámara del dispositivo directamente desde Flutter, sin tener que escribir tu propia integración. Los plugins se encuentran en pub.dev, el repositorio de paquetes de código abierto de Dart y Flutter. Algunos paquetes pueden admitir integraciones nativas en iOS, o Android, o ambos.

Si no puedes encontrar un plugin en pub.dev que se adapte a tus necesidades, puedes escribir el tuyo propio, y publicarlo en pub.dev.

¿Cómo accedo al sensor GPS?

#

Utiliza el plugin de la comunidad geolocator.

¿Cómo accedo a la cámara?

#

El plugin camera es popular para acceder a la cámara.

¿Cómo inicio sesión con Facebook?

#

Para iniciar sesión con Facebook, usa el plugin de la comunidad flutter_facebook_login.

¿Cómo uso las características de Firebase?

#

La mayoría de las funciones de Firebase están cubiertas por plugins oficiales (first party plugins). Estos plugins son integraciones oficiales, mantenidas por el equipo de Flutter:

También puedes encontrar algunos plugins de Firebase de terceros en pub.dev que cubren áreas no cubiertas directamente por los plugins oficiales.

¿Cómo construyo mis propias integraciones nativas personalizadas?

#

Si hay alguna funcionalidad específica de la plataforma que le falte a Flutter o a los plugins de su comunidad, puedes construir la tuya propia siguiendo la página desarrollo de paquetes y plugins.

La arquitectura de plugins de Flutter, en pocas palabras, es muy similar a usar un bus de eventos (Event bus) en Android: lanzas un mensaje y dejas que el receptor procese y emita un resultado de vuelta. En este caso, el receptor es código que se ejecuta en el lado nativo en Android o iOS.

Temas (Estilos)

#

¿Cómo aplico un tema a mi aplicación?

#

Flutter viene con una hermosa implementación integrada de Material Design, que maneja gran parte de las necesidades de diseño y personalización de temas que normalmente realizarías.

Xamarin.Forms tiene un ResourceDictionary global donde puedes compartir estilos en toda tu aplicación. Alternativamente, existe soporte para temas (Theme) actualmente en vista previa.

En Flutter, declaras los temas en el widget de nivel superior.

Para aprovechar al máximo los Material Components en tu aplicación, puedes declarar 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 aplicaciones que implementan Material Design. Se basa en un WidgetsApp agregando funcionalidad específica de Material.

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 siguiente código, el esquema de color a partir de seed se establece en deepPurple y el color de selección de texto es rojo.

dart
class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textSelectionTheme: const TextSelectionThemeData(
          selectionColor: Colors.red,
        ),
      ),
      home: const SampleAppPage(),
    );
  }
}

Bases de datos y almacenamiento local

#

¿Cómo accedo a las preferencias compartidas (shared preferences) o UserDefaults?

#

Los desarrolladores de Xamarin.Forms probablemente estarán familiarizados con el plugin Xam.Plugins.Settings.

En Flutter, accede a la funcionalidad equivalente utilizando el plugin shared_preferences. Este plugin envuelve la funcionalidad tanto de UserDefaults como del equivalente de Android, SharedPreferences.

¿Cómo accedo a SQLite en Flutter?

#

En Xamarin.Forms, la mayoría de las aplicaciones usarían el plugin sqlite-net-pcl para acceder a bases de datos SQLite.

En Flutter, en macOS, Android e iOS, accede a esta funcionalidad utilizando el plugin sqflite.

Depuración

#

¿Qué herramientas puedo usar para depurar mi aplicación en Flutter?

#

Utiliza la suite DevTools para depurar aplicaciones de Flutter o Dart.

DevTools incluye soporte para realizar perfiles (profiling), examinar el montón (heap), inspeccionar el árbol de widgets, registrar diagnósticos, depurar, observar líneas de código ejecutadas, y depurar fugas de memoria (memory leaks) y fragmentación de memoria. Para obtener más información, consulta la documentación de DevTools.

Notificaciones

#

¿Cómo configuro las notificaciones push?

#

En Android, utilizas Firebase Cloud Messaging para configurar notificaciones push para tu aplicación.

En Flutter, accede a esta funcionalidad utilizando el plugin firebase_messaging. Para obtener más información sobre el uso de la API de Firebase Cloud Messaging, consulta la documentación del plugin firebase_messaging.