Flutter para desarrolladores de Android
Aprende cómo aplicar los conocimientos de desarrollador de Android al compilar aplicaciones Flutter.
Este documento está destinado a desarrolladores de Android que buscan aplicar sus conocimientos existentes de Android para compilar aplicaciones móviles con Flutter. Si entiendes los conceptos fundamentales del framework de Android, puedes usar este documento como un punto de partida rápido para el desarrollo con Flutter.
Tus conocimientos y habilidades de Android son sumamente valiosos al compilar con Flutter, porque Flutter depende del sistema operativo móvil para numerosas capacidades y configuraciones. Flutter es una nueva forma de construir UIs para móviles, but it has a plugin system to communicate with Android (and iOS) for non-UI tasks. Si eres un experto en Android, no tienes que volver a aprenderlo todo para usar Flutter.
Este documento se puede utilizar como un recetario (cookbook) saltando de un lado a otro y encontrando las preguntas que sean más relevantes para tus necesidades.
Vistas
#¿Cuál es el equivalente de una View en Flutter?
#En Android, la View es la base de todo lo que aparece en la
pantalla. Botones, barras de herramientas y entradas, todo es una View.
En Flutter, el equivalente aproximado a una View es un Widget.
Los Widgets no se corresponden exactamente con las vistas de Android, pero mientras te
familiarizas con el funcionamiento de Flutter, puedes pensar en ellos como
"la forma en que declaras y construyes la UI".
Sin embargo, estos tienen algunas diferencias con una View. Para empezar, los widgets tienen una
vida útil diferente: son inmutables y solo existen hasta que necesitan ser
cambiados. Cada vez que los widgets o su state cambian, el framework de Flutter crea
un nuevo árbol de instancias de widgets. En comparación, una vista de Android se dibuja una vez
y no se vuelve a dibujar hasta que se llama a invalidate.
Los widgets de Flutter son livianos, en parte debido a su inmutabilidad. Esto se debe a que no son vistas en sí mismos y no dibujan nada directamente, sino que son una descripción de la UI y su semántica que se "infla" en objetos de vista reales tras bambalinas.
Flutter incluye la biblioteca Material Components. Estos son widgets que implementan las pautas 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 como 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 de iOS de Apple.
¿Cómo actualizo los widgets?
#En Android, actualizas tus vistas mutándolas directamente. Sin embargo,
en Flutter, los Widgets son inmutables y no se actualizan directamente,
sino que tienes que trabajar con el state del widget.
De aquí proviene el concepto de widgets Stateful y Stateless.
Un StatelessWidget es justo 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 Android, esto es similar a colocar un ImageView
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 en función de los datos recibidos
después de realizar una llamada HTTP o la interacción del usuario, entonces debes trabajar
con un StatefulWidget y decirle 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
widgets se comportan igual. Se reconstruyen en cada fotograma, la diferencia es que el
StatefulWidget tiene un objeto State que almacena datos de state a través 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 observas la implementación
del widget Text, verás que es una subclase de StatelessWidget.
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 le pasa en sus constructores y nada más.
Pero, ¿qué pasa si quieres hacer que "I Like Flutter" cambie dinámicamente, por
ejemplo al hacer clic en un FloatingActionButton?
Para lograr esto, envuelve el widget Text en un StatefulWidget y
actualízalo cuando el usuario haga clic en el botón.
Por ejemplo:
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const 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? ¿Dónde está mi archivo de diseño XML?
#En Android, escribes los diseños en XML, pero en Flutter escribes tus diseños con un árbol de widgets.
El siguiente ejemplo muestra cómo mostrar un widget simple con padding:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.only(left: 20, right: 30),
),
onPressed: () {},
child: const Text('Hello'),
),
),
);
}
Puedes ver algunos de los diseños que Flutter tiene para ofrecer en el catálogo de widgets.
¿Cómo agrego o elimino un componente de mi diseño?
#En Android, llamas a addChild() o removeChild()
en un padre para agregar o eliminar vistas hijas dinámicamente.
En Flutter, debido a que los widgets son inmutables, no existe
un equivalente directo a addChild(). 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.
Por ejemplo, aquí se muestra cómo puedes alternar entre dos
widgets al hacer clic en un FloatingActionButton:
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const 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');
} else {
return ElevatedButton(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 Android, creas animaciones usando XML o llamas al método animate()
en una vista. En Flutter, animas widgets usando la biblioteca de animaciones
envolviendo los widgets dentro de un widget animado.
En Flutter, usas 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 el 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 una 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, y le dices 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 {
const FadeAppTest({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fade Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const 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 AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: FadeTransition(
opacity: curve,
child: const FlutterLogo(size: 100),
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade',
onPressed: () {
controller.forward();
},
child: const Icon(Icons.brush),
),
);
}
}
Para más información, consulta Widgets de animación y movimiento (Animation & Motion widgets), el tutorial de animaciones (Animations tutorial), y la descripción general de animaciones (Animations overview).
¿Cómo uso un Canvas para dibujar/pintar?
#En Android, usarías el Canvas y el Drawable
para dibujar imágenes y formas en la pantalla.
Flutter también tiene una API de Canvas similar,
ya que se basa en el mismo motor de renderizado de bajo nivel, Skia.
Como resultado, pintar en un canvas en Flutter
es una tarea muy familiar para los desarrolladores de Android.
Flutter tiene dos clases que te ayudan a dibujar en el canvas: CustomPaint
y CustomPainter,
esta última implementa tu algoritmo para dibujar en el canvas.
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>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
Offset localPosition = referenceBox.globalToLocal(
details.globalPosition,
);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child: CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
var 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;
}
¿Cómo construyo widgets personalizados?
#En Android, normalmente creas una subclase de View o utilizas una vista preexistente
para sobrescribir e implementar métodos que logren el comportamiento deseado.
En Flutter, construyes un widget personalizado mediante la composición
de widgets más pequeños (en lugar de extenderlos).
Es algo similar a implementar un ViewGroup personalizado
en Android, donde todos los bloques de construcción ya existen,
pero tú proporcionas un comportamiento diferente, por ejemplo,
lógica de diseño personalizada.
Por ejemplo, ¿cómo construyes un CustomButton que toma una etiqueta en
el constructor? Crea un CustomButton que componga un ElevatedButton
con
una etiqueta, en lugar de extender ElevatedButton:
class CustomButton extends StatelessWidget {
final String label;
const CustomButton(this.label, {super.key});
@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'));
}
Intents
#¿Cuál es el equivalente de un Intent en Flutter?
#En Android, hay dos casos de uso principales para los Intents: navegar entre
Activities y comunicarse con componentes. Flutter, por otro lado,
no tiene el concepto de intents, aunque aún puedes iniciar intents
a través de integraciones nativas (usando un plugin).
Flutter no tiene realmente un equivalente directo a las activities y fragments;
más bien, en Flutter navegas entre pantallas usando un Navigator y
Routes, todo dentro de la misma Activity.
Una Route es una abstracción para una "pantalla" o "página" de una aplicación, y un
Navigator es un widget que administra rutas. Una ruta se asigna aproximadamente a una
Activity, pero no tiene el mismo significado. Un navigator puede hacer
push y pop de rutas para moverse de pantalla en pantalla. Los navigators funcionan como una pila
en la que puedes hacer push() de las nuevas rutas a las que deseas navegar y de la cual
puedes hacer pop() de las rutas cuando deseas "volver atrás".
En Android, declaras tus actividades dentro del archivo AndroidManifest.xml de la aplicación.
En Flutter, tienes un par de opciones para navegar entre páginas:
- Especifica un
Mapde nombres de rutas (usandoMaterialApp). - Navega directamente a una ruta (usando
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 haciendo push de su nombre en el Navigator.
Navigator.of(context).pushNamed('/b');
El otro caso de uso popular para los Intents es llamar a componentes externos como
la Cámara o el selector de archivos (File picker). Para esto, necesitarías crear una integración
con la plataforma nativa (o usar un plugin existente).
Para aprender a compilar una integración con la plataforma nativa, consulta desarrollar paquetes y plugins.
¿Cómo manejo los intents entrantes de aplicaciones externas en Flutter?
#Flutter puede manejar los intents entrantes de Android comunicándose directamente con la capa de Android y solicitando los datos que se compartieron.
El siguiente ejemplo registra un filtro de intent para compartir texto en la actividad nativa que ejecuta nuestro código Flutter, de modo que otras aplicaciones puedan compartir texto con nuestra aplicación Flutter.
El flujo básico implica que primero manejamos los datos de texto compartidos en el
lado nativo de Android (en nuestra Activity) y luego esperamos hasta que Flutter solicite
los datos para proporcionarlos usando un MethodChannel.
Primero, registra el filtro de intent para todos los intents en AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- ... -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
Luego, en MainActivity, maneja el intent, extrae el texto que se
compartió desde el intent y consérvalo. Cuando Flutter esté listo para procesarlo,
solicitará los datos a través de un canal de plataforma, y se enviará
desde el lado nativo:
package com.example.shared;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
private String sharedText;
private static final String CHANNEL = "app.channel.shared.data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent); // Handle text being sent
}
}
}
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
);
}
void handleSendText(Intent intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
}
}
Finalmente, solicita los datos desde el lado de Flutter cuando el widget se renderice:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample Shared App Handler',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
static const platform = MethodChannel('app.channel.shared.data');
String dataShared = 'No data';
@override
void initState() {
super.initState();
getSharedText();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(dataShared)));
}
Future<void> getSharedText() async {
var sharedData = await platform.invokeMethod('getSharedText');
if (sharedData != null) {
setState(() {
dataShared = sharedData as String;
});
}
}
}
¿Cuál es el equivalente de startActivityForResult()?
#La clase Navigator maneja el enrutamiento en Flutter y se utiliza para recuperar
un resultado de una ruta a la que has hecho push en la pila.
Esto se logra haciendo await en el Future devuelto por push().
Por ejemplo, para iniciar una ruta de ubicación 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 ubicación, una vez que el usuario haya seleccionado su ubicación,
puedes hacer pop en la pila con el resultado:
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
UI asíncrona
#¿Cuál es el equivalente de runOnUiThread() en Flutter?
#Dart tiene un modelo de ejecución de un solo hilo (single-threaded), con soporte para Isolates
(una forma de ejecutar código Dart en otro hilo), un bucle de eventos (event loop) y
programación asíncrona. A menos que generes un Isolate, tu código Dart
se ejecuta en el hilo principal de la UI y es impulsado por un bucle de eventos. El bucle de eventos
de Flutter es equivalente al Looper principal de Android, es decir, el Looper
que está adjunto al hilo principal.
El modelo de un solo hilo de Dart no significa que debas ejecutar todo como una
operación bloqueante que cause que la UI se congele. A diferencia de Android, que
requiere que mantengas libre el hilo principal en todo momento, en Flutter
utilizas las facilidades asíncronas que proporciona el lenguaje Dart, como
async/await, para realizar trabajo asíncrono. Es posible que estés familiarizado con
el paradigma async/await si lo has usado en C#, Javascript o si
has usado las corrutinas de Kotlin.
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 dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
Una vez que finaliza la llamada de red esperada con await, actualizas 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 MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
}
Consulta la siguiente sección para obtener más información sobre cómo realizar trabajo en el segundo plano y en qué se diferencia Flutter de Android.
¿Cómo se mueve el trabajo a un hilo de fondo?
#En Android, cuando deseas acceder a un recurso de red, normalmente te
moverías a un hilo de fondo (background thread) y realizarías el trabajo, para no bloquear el hilo principal
y evitar los ANR. Por ejemplo, podrías estar utilizando un AsyncTask, un LiveData,
un IntentService, una tarea de JobScheduler o una canalización de RxJava con un
planificador que trabaje en hilos de fondo.
Dado que Flutter es monohilo (single threaded) y ejecuta un bucle de eventos (como Node.js), no
tienes que preocuparte por la gestión de hilos o por generar hilos de fondo. Si
estás haciendo trabajo de entrada/salida (I/O-bound), como acceso al disco o una llamada de red,
puedes usar async/await de forma segura y listo. Si, por el contrario,
necesitas hacer un trabajo computacionalmente intensivo que mantenga ocupada la CPU,
querrás moverlo a un Isolate para evitar bloquear el bucle de eventos, al igual que
mantendrías cualquier tipo de trabajo fuera del hilo principal en Android.
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 dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
Así es como normalmente harías llamadas de red o de bases de datos, que son operaciones de entrada/salida.
En Android, cuando extiendes de AsyncTask, normalmente sobrescribes 3 métodos,
onPreExecute(), doInBackground() y onPostExecute(). No hay
equivalente en Flutter, ya que haces await en una función de larga duración y
el bucle de eventos de Dart se encarga del resto.
Sin embargo, hay ocasiones en las que podrías estar procesando una gran cantidad de datos y
tu UI se cuelga. En Flutter, usas Isolates para aprovechar
múltiples núcleos de CPU para realizar tareas de larga duración o computacionalmente intensivas.
Los Isolates son hilos de ejecución separados que no comparten memoria
con el heap de memoria de ejecución principal. Esto significa que no puedes acceder a variables desde
el hilo principal ni actualizar la UI llamando a setState().
A diferencia de los hilos de Android,
los Isolates hacen honor a su nombre y no pueden compartir memoria
(en forma de campos estáticos, por ejemplo).
El siguiente ejemplo muestra, en un isolate simple, cómo compartir datos de vuelta al hilo principal para actualizar la UI.
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first as SendPort;
final msg =
await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
)
as List<Object?>;
final posts = msg.cast<Map<String, Object?>>();
setState(() {
widgets = posts;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String dataUrl = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
http.Response response = await http.get(Uri.parse(dataUrl));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future<Object?> sendReceive(SendPort port, Object? msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
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 cálculos matemáticos computacionalmente intensivos,
como el cifrado o el 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 MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> widgets = [];
@override
void initState() {
super.initState();
loadData();
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
ListView getListView() {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first as SendPort;
final msg =
await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
)
as List<Object?>;
final posts = msg.cast<Map<String, Object?>>();
setState(() {
widgets = posts;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String dataUrl = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
http.Response response = await http.get(Uri.parse(dataUrl));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future<Object?> sendReceive(SendPort port, Object? msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
¿Cuál es el equivalente de OkHttp en Flutter?
#Realizar una llamada de red en Flutter es fácil cuando usas el
popular paquete http.
Aunque el paquete http no tiene todas las características que se encuentran en OkHttp, abstrae gran parte de la conectividad de red que normalmente implementarías tú mismo, lo que lo convierte en una forma sencilla de realizar llamadas de red.
Para agregar el paquete http como una dependencia, ejecuta flutter pub add:
flutter pub add http
Para realizar una llamada de red, llama a await en la función async http.get():
import 'dart:developer' as developer;
import 'package:http/http.dart' as http;
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
developer.log(response.body);
}
¿Cómo muestro el progreso de una tarea de larga duración?
#En Android, normalmente mostrarías una vista de ProgressBar en tu UI mientras
ejecutas una tarea de larga duración en un hilo de fondo.
En Flutter, usas un widget ProgressIndicator.
Muestra el progreso programáticamente controlando cuándo se renderiza
a través de una bandera booleana. Dile 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 siguiente ejemplo, la función build se separa en tres funciones diferentes.
Si showLoadingDialog es true (cuando widgets.isEmpty),
entonces renderiza el ProgressIndicator. De lo contrario, renderiza el
ListView con los datos devueltos por una llamada de red.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> widgets = [];
@override
void initState() {
super.initState();
loadData();
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
ListView getListView() {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
}
Estructura del proyecto y recursos
#¿Dónde almaceno mis archivos de imagen que dependen de la resolución?
#Mientras que Android trata los recursos y los assets como elementos distintos,
las aplicaciones de Flutter solo tienen assets. Todos los recursos que vivirían
en las carpetas res/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 ubican 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.
Se accede a los assets almacenados en la carpeta de assets nativos en el
lado nativo utilizando el AssetManager de Android:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
Flutter no puede acceder a recursos o assets nativos.
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
Luego puedes acceder a tus imágenes usando AssetImage:
AssetImage('images/my_icon.png'),
o directamente en un widget Image:
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
¿Dónde almaceno las cadenas de texto? ¿Cómo manejo la localización?
#Flutter actualmente no tiene un sistema dedicado similar a recursos para cadenas de texto.
La mejor práctica recomendada es guardar las cadenas en un archivo .arb como pares clave-valor. Por ejemplo:
{
"@@locale": "en",
"hello":"Hello {userName}",
"@hello":{
"description":"A message with a single parameter",
"placeholders":{
"userName":{
"type":"String",
"example":"Bob"
}
}
}
}
Luego, en tu código, puedes acceder a tus cadenas de la siguiente manera:
Text(AppLocalizations.of(context)!.hello('John'));
Consulta Internacionalización de aplicaciones Flutter (Internationalizing Flutter apps) para obtener más información al respecto.
¿Cuál es el equivalente de un archivo Gradle? ¿Cómo agrego dependencias?
#En Android, agregas dependencias añadiéndolas a tu script de compilación de Gradle. 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 contenedoras nativas de Android e iOS a los respectivos sistemas de compilación.
Aunque hay archivos Gradle en la carpeta android de tu
proyecto Flutter, solo utilízalos si vas a agregar dependencias
nativas necesarias para la integración por plataforma.
En general, utiliza pubspec.yaml para declarar
dependencias externas que usarás en Flutter.
Un buen lugar para encontrar paquetes de Flutter es pub.dev.
Activities y fragments
#¿Cuáles son los equivalentes de las activities y fragments en Flutter?
#En Android, una Activity representa una única cosa en la que el usuario puede enfocarse.
Un Fragment representa un comportamiento o una parte de la interfaz de usuario.
Los Fragments son una forma de modularizar tu código, componer interfaces de usuario
sofisticadas para pantallas más grandes y ayudar a escalar la UI de tu aplicación.
En Flutter, ambos conceptos caen bajo el paraguas de los Widgets.
Para obtener más información sobre la UI para compilar Activities y Fragments, consulta el artículo aportado por la comunidad, Flutter for Android Developers: How to design Activity UI in Flutter.
Como se mencionó en la sección Intents,
las pantallas en Flutter están representadas por Widgets, ya que todo es
un widget en Flutter. Utiliza un Navigator para moverte entre diferentes
Routes que representan diferentes pantallas o páginas,
o tal vez diferentes states o representaciones de los mismos datos.
¿Cómo escucho los eventos del ciclo de vida de la activity de Android?
#En Android, puedes sobrescribir métodos de la Activity para capturar métodos del
ciclo de vida de la actividad en sí, o registrar ActivityLifecycleCallbacks
en
la Application. En Flutter, no tienes ninguno de los dos conceptos, pero en su lugar
puedes escuchar eventos del ciclo de vida vinculándote al observador WidgetsBinding y
escuchando el evento de cambio didChangeAppLifecycleState().
Los eventos del ciclo de vida observables son:
detached(desacoplada) — La aplicación todavía está alojada en un motor Flutter, pero está desacoplada de cualquier vista host.inactive(inactiva) — La aplicación está en un estado inactivo y no recibe información del usuario.paused(pausada) — La aplicación no es visible actualmente para el usuario, no responde a la entrada del usuario y se ejecuta en segundo plano. Esto equivale aonPause()en Android.resumed(reanudada) — La aplicación es visible y responde a la entrada del usuario. Esto equivale aonPostResume()en Android.
Para obtener más detalles sobre el significado de estos estados, consulta la
documentación de AppLifecycleStatus.
Como habrás notado, solo está disponible una pequeña minoría de los eventos del ciclo de vida de la Activity;
aunque FlutterActivity captura internamente casi todos los eventos del ciclo de vida de la actividad
y los envía al motor de Flutter, en su mayoría están protegidos de ti.
Flutter se encarga de iniciar y detener el motor por ti, y hay pocas razones
para necesitar observar el ciclo de vida de la actividad en el lado de Flutter en la mayoría de los casos.
Si necesitas observar el ciclo de vida para adquirir o liberar recursos nativos,
probablemente deberías hacerlo desde el lado nativo de todos modos.
Aquí tienes un ejemplo de cómo observar el estado del ciclo de vida de la actividad contenedora:
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
const LifecycleWatcher({super.key});
@override
State<LifecycleWatcher> createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher>
with WidgetsBindingObserver {
AppLifecycleState? _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null) {
return const Text(
'This widget has not observed any lifecycle changes.',
textDirection: TextDirection.ltr,
);
}
return Text(
'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr,
);
}
}
void main() {
runApp(const Center(child: LifecycleWatcher()));
}
Diseños (Layouts)
#¿Cuál es el equivalente de un LinearLayout?
#En Android, se utiliza un LinearLayout para distribuir tus widgets de forma lineal, ya sea de forma horizontal o vertical. En Flutter, utilizas los widgets Row o Column para lograr el mismo resultado.
Si te fijas, los dos ejemplos de código son idénticos a excepción de los widgets "Row" y "Column". Los hijos son los mismos y esta característica se puede aprovechar para desarrollar diseños enriquecidos 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'),
],
);
}
Para obtener más información sobre cómo construir diseños lineales, consulta el artículo de Medium aportado por la comunidad Flutter for Android Developers: How to design LinearLayout in Flutter.
¿Cuál es el equivalente de un RelativeLayout?
#Un RelativeLayout distribuye tus widgets de forma relativa entre sí. En Flutter, hay algunas formas de lograr el mismo resultado.
Puedes lograr el resultado de un RelativeLayout utilizando una combinación de widgets Column, Row y Stack. Puedes especificar reglas en los constructores de los widgets sobre cómo se distribuyen los hijos en relación con el padre.
Para ver un buen ejemplo de cómo construir un RelativeLayout en Flutter, consulta la respuesta de Collin en StackOverflow.
¿Cuál es el equivalente de un ScrollView?
#En Android, utilizas un ScrollView para distribuir tus widgets: si el dispositivo del usuario tiene una pantalla más pequeña que tu contenido, este se desplaza.
En Flutter, la forma más fácil de hacer esto es usando el widget ListView. Esto podría parecer excesivo viniendo de Android, pero en Flutter un widget ListView es tanto un ScrollView como un ListView de Android.
@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?
#FlutterView maneja el cambio de configuración si AndroidManifest.xml contiene:
android:configChanges="orientation|screenSize"
Detección de gestos y manejo de eventos táctiles
#¿Cómo agrego un listener onClick a un widget en Flutter?
#En Android, puedes adjuntar un onClick a vistas como botones llamando al método 'setOnClickListener'.
En Flutter hay dos formas de agregar listeners táctiles:
- Si el Widget admite la detección de eventos, pásale una función y gestiónalo
en ella. Por ejemplo, ElevatedButton tiene un parámetro
onPressed:
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- Si el Widget no admite la detección de eventos, envuelve el
widget en un GestureDetector y pasa una función al parámetro
onTap.
class SampleTapApp extends StatelessWidget {
const SampleTapApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
developer.log('tap');
},
child: const FlutterLogo(size: 200),
),
),
);
}
}
¿Cómo manejo otros gestos en los widgets?
#Usando el GestureDetector, puedes escuchar una amplia gama de gestos como:
-
Tap
onTapDown- Un puntero que podría causar un toque ha hecho contacto con la pantalla en una ubicación específica.onTapUp- Un puntero que activa un toque ha dejado de hacer contacto con la pantalla en una ubicación específica.onTap- Se ha producido un toque.onTapCancel- El puntero que previamente activó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
onVerticalDragStart- Un puntero ha hecho 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 dirección vertical.onVerticalDragEnd- Un puntero que anteriormente estaba en contacto con la pantalla y moviéndose verticalmente ya no está en contacto con la pantalla, y se movía a una velocidad específica cuando dejó de hacer contacto con ella.
-
Arrastre horizontal
onHorizontalDragStart- Un puntero ha hecho contacto con la pantalla y podría comenzar a moverse horizontalmente.onHorizontalDragUpdate- Un puntero en contacto con la pantalla se ha movido más en dirección horizontal.onHorizontalDragEnd- Un puntero que anteriormente estaba en contacto con la pantalla y moviéndose horizontalmente ya no está en contacto con la pantalla, y se movía a una velocidad específica cuando dejó de hacer contacto con ella.
El siguiente ejemplo muestra un GestureDetector
que rota el logotipo de Flutter al hacer doble toque:
class SampleApp extends StatefulWidget {
const SampleApp({super.key});
@override
State<SampleApp> createState() => _SampleAppState();
}
class _SampleAppState extends State<SampleApp>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@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 la alternativa a un ListView en Flutter?
#El equivalente a un ListView en Flutter es… ¡un ListView!
En un ListView de Android, creas un adaptador y lo pasas al ListView, el cual renderiza cada fila con lo que devuelve tu adaptador. Sin embargo, debes asegurarte de reciclar las filas; de lo contrario, obtendrás todo tipo de fallos visuales extraños y problemas de memoria.
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 {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
return widgets;
}
}
¿Cómo sé en qué elemento de la lista se hace clic?
#En Android, el ListView tiene un método para saber qué elemento se hizo clic, 'onItemClickListener'. En Flutter, utilizas el manejo táctil proporcionado por los widgets pasados.
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
}
¿Cómo actualizo los ListView dinámicamente?
#En Android, actualizas el adaptador y llamas a notifyDataSetChanged.
En Flutter, si actualizaras la lista de widgets dentro de un 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
examina 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.
Aunque este enfoque es simple, no se recomienda para conjuntos de datos grandes,
como se muestra en el siguiente ejemplo.
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const 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));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
}
La forma recomendada, eficiente y eficaz de construir una lista utiliza un
ListView.Builder. Este método es excelente cuando tienes una
List dinámica o una List con cantidades de datos muy grandes. Esto es esencialmente
el equivalente de RecyclerView en Android, que recicla automáticamente
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 {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const 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));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
}
En lugar de crear un "ListView", crea un
ListView.builder que tome dos parámetros clave: la
longitud inicial de la lista y una función ItemBuilder.
La función ItemBuilder es similar a la función getView
en un adaptador de Android; toma una posición
y devuelve la fila que deseas renderizar en esa posición.
Finalmente, pero lo más importante, nota que la función onTap()
ya no recrea la lista, sino que hace .add (añade) en ella.
Trabajar con texto
#¿Cómo configuro fuentes personalizadas en mis widgets de Text?
#En el SDK de Android (a partir de Android O), creas un archivo de recurso de fuente (Font) y lo pasas al parámetro FontFamily para tu TextView.
En Flutter, colocas el archivo de fuente en una carpeta y lo referencias en el
archivo pubspec.yaml, de forma similar a como 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 aplico estilos a mis widgets de Text?
#Junto con las fuentes, puedes personalizar otros elementos de estilo en un widget Text.
El parámetro style de un widget Text toma 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
#Para obtener más información sobre el uso de formularios, consulta Recuperar el valor de un campo de texto (Retrieve the value of a text field).
¿Cuál es el equivalente de un "hint" en un Input?
#En Flutter, puedes mostrar fácilmente un "hint" o un texto de marcador de posición (placeholder) para tu entrada agregando un objeto InputDecoration al parámetro de constructor decoration del Widget Text.
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)
¿Cómo muestro los errores de validación?
#Al igual que lo harías con un "hint", pasa un objeto InputDecoration al constructor decoration del widget Text.
Sin embargo, no querrás empezar mostrando un error.
En su lugar, cuando el usuario haya introducido datos no válidos,
actualiza el state y pasa un nuevo objeto InputDecoration.
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String? _errorText;
@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(),
),
),
),
);
}
String? _getErrorText() {
return _errorText;
}
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
}
Plugins de Flutter
#¿Cómo accedo al sensor GPS?
#Utiliza el plugin de la comunidad geolocator.
¿Cómo accedo a la cámara?
#El plugin image_picker es popular
para acceder a la cámara.
¿Cómo inicio sesión con Facebook?
#Para iniciar sesión con Facebook, utiliza 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 que no están cubiertas directamente por los plugins oficiales.
¿Cómo construyo mis propias integraciones nativas personalizadas?
#Si hay alguna funcionalidad específica de la plataforma de la que carecen Flutter o los plugins de su comunidad, puedes compilar la tuya propia siguiendo la página desarrollar paquetes y plugins (developing packages and 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 lo procese y te devuelva un resultado. En este caso, el receptor es el código que se ejecuta en el lado nativo en Android o iOS.
¿Cómo uso el NDK en mi aplicación Flutter?
#Si usas el NDK en tu aplicación de Android actual y deseas que tu aplicación Flutter aproveche tus bibliotecas nativas, es posible construyendo un plugin personalizado.
Tu plugin personalizado primero se comunica con tu aplicación de Android, donde llamas a tus
funciones native a través de JNI. Una vez que la respuesta esté lista,
envía un mensaje de vuelta a Flutter y renderiza el resultado.
Actualmente no se admite la llamada a código nativo directamente desde Flutter.
Temas
#¿Cómo aplico un tema a mi aplicación?
#De forma predeterminada, Flutter viene con una hermosa implementación de Material Design, que se encarga de muchas de las necesidades de estilo y temas que normalmente tendrías que hacer. A diferencia de Android, donde declaras los temas en XML y luego los asignas a tu aplicación usando AndroidManifest.xml, en Flutter declaras los temas en el widget de nivel superior.
Para aprovechar al máximo Material Components en tu aplicación, puedes declarar un widget
de nivel superior MaterialApp como punto de entrada a tu aplicación. MaterialApp
es un widget de conveniencia que envuelve una serie de widgets que normalmente se
requieren para las aplicaciones que implementan Material Design.
Se basa en un WidgetsApp agregando funcionalidad específica de Material.
También puedes usar un WidgetsApp como el widget de tu aplicación, el cual proporciona parte de la
misma funcionalidad, pero no es tan enriquecido como MaterialApp.
Para personalizar los colores y estilos de cualquier componente hijo, pasa un
objeto ThemeData al widget MaterialApp. Por ejemplo, en el código siguiente,
el esquema de color a partir de la semilla se establece en deepPurple y el color de selección de texto es rojo.
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: Colors.red,
),
),
home: const SampleAppPage(),
);
}
}
Widgets de la pantalla de inicio
#¿Cómo creo un widget para la pantalla de inicio?
#Los widgets de la pantalla de inicio de Android no se pueden crear completamente con Flutter. Deben usar Jetpack Glance (método preferido) o código de diseño XML. Al usar el paquete de terceros home_widget, puedes conectar un widget de la pantalla de inicio al código de Dart, embeber un componente de Flutter (como una imagen) en un widget host y compartir datos desde/hacia Flutter con el widget de la pantalla de inicio.
Para proporcionar una experiencia más enriquecida y atractiva, se recomienda agregar vistas previas de widgets para incluirlas en el selector de widgets. Para dispositivos con Android 15 y versiones superiores, las vistas previas de widgets generadas permiten al usuario ver una versión dinámica y personalizada del widget de destino, dándoles una idea de cómo se verá exactamente en su pantalla de inicio. Para obtener más información sobre las vistas previas de widgets generadas y las opciones alternativas para dispositivos más antiguos, consulta la página de documentación Agregar vistas previas generadas a tu selector de widgets.
Bases de datos y almacenamiento local
#¿Cómo accedo a las Shared Preferences?
#En Android, puedes almacenar una pequeña colección de pares clave-valor utilizando la API SharedPreferences.
En Flutter, accedes a esta funcionalidad utilizando el Shared_Preferences plugin. Este plugin envuelve la funcionalidad tanto de Shared Preferences como de NSUserDefaults (el equivalente en iOS).
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
Future<void> _incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
await prefs.setInt('counter', counter);
}
¿Cómo accedo a SQLite en Flutter?
#En Android, utilizas SQLite para almacenar datos estructurados que puedes consultar usando SQL.
En Flutter, para macOS, Android o iOS, accedes 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 el análisis de rendimiento (profiling), examinar el heap, inspeccionar el árbol de widgets, registrar diagnósticos, depuración, 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, accedes 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 en este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 25-06-2026. Ver código fuente oreportar un problema.