Saltar al contenido principal

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.

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

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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:

dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Sample App')),
    body: Center(
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.only(left: 20, right: 30),
        ),
        onPressed: () {},
        child: const Text('Hello'),
      ),
    ),
  );
}

Puedes ver 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:

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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:

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

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

void main() => runApp(const MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) => const Scaffold(body: Signature());
}

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

  @override
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset>[];
  @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:

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

dart
@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 Map de nombres de rutas (usando MaterialApp).
  • Navega directamente a una ruta (usando WidgetsApp).

El siguiente ejemplo construye un Map.

dart
void main() {
  runApp(
    MaterialApp(
      home: const MyAppHome(), // Becomes the route named '/'.
      routes: <String, WidgetBuilder>{
        '/a': (context) => const MyPage(title: 'page A'),
        '/b': (context) => const MyPage(title: 'page B'),
        '/c': (context) => const MyPage(title: 'page C'),
      },
    ),
  );
}

Navega a una ruta haciendo push de su nombre en el Navigator.

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

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:

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

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

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

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

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

dart
import 'dart:convert';

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  @override
  Widget build(BuildContext context) {
    return 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:

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

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

dart
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return 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():

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

dart
import 'dart:convert';

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return 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 AndroidRelación de píxeles de Flutter
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.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:

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

yaml
assets:
 - images/my_icon.png

Luego puedes acceder a tus imágenes usando AssetImage:

dart
AssetImage('images/my_icon.png'),

o directamente en un widget Image:

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

json
{
   "@@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:

dart
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 a onPause() en Android.
  • resumed (reanudada) — La aplicación es visible y responde a la entrada del usuario. Esto equivale a onPostResume() 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:

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

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

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.

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

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

#

FlutterView maneja el cambio de configuración si AndroidManifest.xml contiene:

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

  1. 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:
dart
@override
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () {
      developer.log('click');
    },
    child: const Text('Button'),
  );
}
  1. Si el Widget no admite la detección de eventos, envuelve el widget en un GestureDetector y pasa una función al parámetro onTap.
dart
class SampleTapApp extends StatelessWidget {
  const SampleTapApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onTap: () {
            developer.log('tap');
          },
          child: const FlutterLogo(size: 200),
        ),
      ),
    );
  }
}

¿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ó onTapDown no causará un toque.
  • Doble tap

    • onDoubleTap - El usuario tocó la pantalla en la misma ubicación dos veces en rápida sucesión.
  • Presión prolongada

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

    • onVerticalDragStart - Un puntero ha 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:

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

  @override
  State<SampleApp> createState() => _SampleAppState();
}

class _SampleAppState extends State<SampleApp>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late CurvedAnimation curve;

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

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

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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.

dart
import 'dart:developer' as developer;

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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.

dart
import 'dart:developer' as developer;

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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:

dart
import 'dart:developer' as developer;

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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.

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

Luego asignas la fuente a tu widget Text:

dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Sample App')),
    body: const Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

¿Cómo 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.

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

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return 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:

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.

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        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).

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