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.
LoadApplication(new App());
En Flutter, el punto de entrada principal predeterminado es
main donde cargas tu aplicación Flutter.
void main() {
runApp(const MyApp());
}
En Xamarin.Forms, asignas una Page a la
propiedad MainPage en la clase Application.
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.
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.
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.
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.
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.
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:
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:
@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:
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.
<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.
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:
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.
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:
class CustomButton extends StatelessWidget {
const CustomButton(this.label, {super.key});
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}
Luego usa CustomButton, tal como usarías cualquier otro widget de Flutter:
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}
Navegación
#¿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
Mapde nombres de rutas. (MaterialApp) - Navegar directamente a una ruta. (
WidgetsApp)
El siguiente ejemplo construye un Map.
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.
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:
Object? coordinates = await Navigator.of(context).pushNamed('/location');
Y luego, dentro de tu ruta de 'location', una vez que el usuario haya seleccionado su ubicación, realiza un pop en la pila con el resultado:
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
¿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:
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:
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:
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.
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Aquí, dataLoader() es el Isolate que se ejecuta en
su propio hilo de ejecución separado.
En el isolate, puedes realizar un procesamiento más intensivo de la CPU
(analizar un JSON grande, por ejemplo),
o realizar operaciones matemáticas computacionalmente intensivas,
como cifrado o procesamiento de señales.
Puedes ejecutar el ejemplo completo a continuación:
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Widget getBody() {
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:
dependencies:
http: ^1.4.0
Para realizar una solicitud de red,
llama a await en la función async http.get():
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
¿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.
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 Android | Relación de píxeles de Flutter |
|---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.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:
assets:
- images/my_icon.png
Puedes acceder directamente a tus imágenes en un widget Image.asset:
@override
Widget build(BuildContext context) {
return Image.asset('images/my_icon.png');
}
o utilizando AssetImage:
@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:
class Strings {
static const String welcomeMessage = 'Welcome To Flutter';
}
Puedes acceder a tus strings de esta manera:
Text(Strings.welcomeMessage);
Por defecto, Flutter solo admite inglés de EE. UU. para sus strings.
Si necesitas agregar soporte para otros idiomas,
incluye el paquete flutter_localizations.
También es posible que necesites agregar el paquete intl de Dart
para usar el mecanismo de localización (i18n), como el formato de fecha/hora.
dependencies:
flutter_localizations:
sdk: flutter
intl: any # Use version of intl from flutter_localizations.
Para usar el paquete flutter_localizations,
especifica los localizationsDelegates y
supportedLocales en el widget de la aplicación:
import 'package:flutter_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.
resumedLa 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.
@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'),
],
);
}
@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.
@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í.
@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.
@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.
@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:
<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:
-
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'), ); } -
Si el widget no admite la detección de eventos, envuelve el widget en un
GestureDetectory pasa una función al parámetroonTap.dartclass 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.
onTapHa ocurrido un toque.
onTapCancel-
El puntero que anteriormente activó el
onTapDownno 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
onVerticalDragStartUn 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
onHorizontalDragStartUn 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:
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.
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.
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.
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:
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.
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
Luego asignas la fuente a tu widget Text:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: const Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
¿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:
colordecorationdecorationColordecorationStylefontFamilyfontSizefontStylefontWeighthashCodeheightinheritletterSpacingtextBaselinewordSpacing
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:
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:
<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.
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.
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:
google_mobile_adspara Google Mobile Ads para Flutterfirebase_analyticspara Firebase Analyticsfirebase_authpara Firebase Authfirebase_databasepara Firebase RTDBfirebase_storagepara Firebase Cloud Storagefirebase_messagingpara Firebase Messaging (FCM)flutter_firebase_uipara integraciones rápidas de Firebase Auth (Facebook, Google, Twitter y correo electrónico)cloud_firestorepara Firebase Cloud Firestore
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.
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.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.