Curso de Flutter

¿Qué es Flutter?

Es un SDK (Software Development Kit) creado por Google para diseñar interfaces nativas para Android y IPhone, asímo applicaciones de escritorio y aplicaciones web.

Tiene un alto enfoque en el rendimiento. Las aplicaciones desarrolladas con Flutter ofrecen una experiencia realmente nativa de uso.

En el año 2018 salió la primera versión estable de Flutter.

El lenguaje de programación que utiliza es Dart.

Toma muchas ideas de funcionamiento de React.

En la página https://startflutter.com/ podremos consultar multitud de pequeños ejemplos para Flutter.

¿Qué es Dart?

Es un lenguaje de programación creado por Google para programadores de interfaces móviles.

Su página es https://dart.dev/.

En https://dartpad.dev/ puedes probar códigos de Dart online.

Características del lenguaje Dart

En la mayoría de los casos, es posible omitir el tipo de dato:

 selectDate(BuildContext context) async {
    final DateTime? picked = await showDatePicker(

Podría dejarse así:

 selectDate( context) async {
    final picked = await showDatePicker(

Es posible no usar la palabra reservada new:

measure =  new TextEditingController();

Quedaría así:

measure =  TextEditingController();

Fichero de constantes

En Dart puedes declarar constantes estáticas fuera de una clase. En estos casos, no será necesario el modificador static.

 const  String APP_NAME = 'El diario de Papa';
import 'utils/consts.dart';
...
appBar: AppBar(
   title: Text(APP_NAME),
),

El editor de código

Tenemos dos opciones: Visual Studio Code o Android Studio.

El Android Studio tiene mucho mayores requisitos de potencia. No obstante, si la máquina lo permite, es recomendable utilizar el Android Studio por encima del Visual Studio.

Instalación de Flutter

  1. Descargamos el SDK de Flutter https://flutter.dev/docs/get-started/install.
  2. Tanto si vamos a usar Visual Studio Code o Android Studio para desarrollar, instalamos las extensiones de Flutter y Dart.
  3. Para poder ejecutar los comandos de Flutter, sería muy útil añadir esta la carpeta bin de flutter a las variables de entorno de tu sistema operativo.
  4. ctrl + shift + p → Flutter: New Application Project.

El editor

Estructura de un proyecto en Flutter

En la raíz de un proyecto Flutter tenemos las siguientes carpetas:

  • Android → Ficheros específicos para la compilación de un proyecto en Android.
  • ios → Ficheros específicos para la compilación de un proyecto in ios.
  • web → Ficheros específicos para la compilación de un proyecto web.
  • lib → Aquí estaremos trabajando el 99.9% del tiempo. Aquí está el código fuente de nuestra aplicación, compartido para todas las plataformas.
  • pubspec.yaml → Fichero de configuración en el que deben estar referenciados todos los assets que cargue la aplicación (video, audio, imágenes, fuentes…)

Compilación

Para Android

Compilación en modo debug. Generará un apk en build/app/outputs/apk

flutter build apk
flutter build apk --debug

Para web

flutter build web

Si vamos a instalar la aplicación en una subcarpeta tendremos que sobreescribir la ruta en la siguiente etiqueta del fichero /web/index.html:

<base href="$FLUTTER_BASE_HREF">

Hola Mundo con Flutter

Carga los Widgets del SDK de Flutter.

import 'package:flutter/material.dart';

Definimos el punto de arranque de nuestra aplicación. Arrancamos el Widget MyApp.

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp();

  // El método build de la clase StatelessWidget nos devuelve el Widget que será visualizado
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Este título identifica a la aplicación pero lo vemos, por ejemplo, cuando activamos el Task Manager para navegar entre nuestras aplicaciones
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.green, // Color principal de la plantilla usada
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Hello World Title"),
        ),
        body: Center(child: Text("Hello World")),
      ),
    );
  }
}

Widgets

Un Widget en Flutter, es el equivalente a un View en Android y en React Native o a un div en HTML.

En Android desarrollamos interfaces a partir de un documento XML. En Flutter lo hacemos a través del Widget Tree.

Cuando en Android Studio pulsamos sobre un Widget, nos saldrá una bombilla amarilla opciones para envolverlo en otro Widget, o borrar el Widget que nos serán muy cómodas para desarrollar. También podemos acceder a estas opciones pulsando alt + Enter.

Curso de Flutter 1

El árbol de widgets

Es el Widget que devuelve el método build.

Widget build(BuildContext context) {

Es recomendable que todos los elementos del árbol de Widgets vayan separados entre sí por coma, con esto lograremos que al formatear nuestro código se separen de forma más clara.

Scaffold

El Scaffold de un Widget es el esqueleto ( o literalmente traducido andamio) en el que se sostienen todos sus elementos visuales.

El Scaffold de un Widget es el esqueleto ( o literalmente traducido andamio) en el que se sostienen todos sus elementos visuales.

Scaffold(
  appBar:,
  body:,
  bottomNavigationBar:,
  floatingActionButton:,
  floatingActionButtonLocation:,
);

Curso de Flutter 2
return Scaffold(
    appBar: AppBar(
      title: const Text('Floating Action Button'),
    ),
    body: const Center(
      child: Text('Press the button below!')
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        // Add your onPressed code here!
      },
      child: const Icon(Icons.navigation),
      backgroundColor: Colors.green,
    ),
  );

Text

Documentación con ejemplos: https://api.flutter.dev/flutter/widgets/Text-class.html

Text(
  'Hello, $_name! How are you?',
  textAlign: TextAlign.center,
  style: const TextStyle(
    fontWeight: FontWeight.bold
    backgroundColor: Colors.blue
  ),
)

Si quisieramos poner un padding a un Text, necesitaríamos un container:

Container(
  color: Colors.yellow,
  padding: const EdgeInsets.all(12),
  child: Text('Peso',
  style: TextStyle( fontSize: 40),
  ),
),

Si quisieramos alinear a la izquierda un texto que está dentro de una columna, es probable que tengamos que asignar a la columna la propiedad:

crossAxisAlignment: CrossAxisAlignment.start,

Textfield

class MyClass extends State<AddItem> {
  TextEditingController? textEditingControllerItem;

  void initState(){
    super.initState();
    textEditingControllerItem = TextEditingController();
  }

...

TextField(
  controller: textEditingControllerWeight,
  keyboardType: TextInputType.number,
  decoration: new InputDecoration.collapsed(
    hintText: 'Introduce el peso del niño'
  ),
),

Column y Row

Column: por defecto, su ancho es el ancho del contenido y su alto, el alto la pantalla.

Row: por defecto su ancho es el ancho de la pantalla y su alto, el alto del contenido.

Esto puede ocasionar que su contenido no se centre cuando estamos dentro de un Widget Center. Para evitarlo, podemos usar la propiedad:

mainAxisSize: MainAxisSize.min,

Stack y Container

Stack nos permite hacer superposición de elementos.

Container es cajita en la que podemos meter información. Equivaldría a un div, al que puedes meter un padding, un margin, un border…

Stack(
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 90,
      height: 90,
      color: Colors.green,
    ),
    Container(
      width: 80,
      height: 80,
      color: Colors.blue,
    ),
  ],
)
  • En el caso de que un Container no tenga unas dimensiones (width y height) asignadas, adaptará su tamaño a su contenido.
  • El container tiene un único hijo.
  • Un Container que tiene propiedad color no puede tener Widget Decoration, y viceversa.

Aceptan una colección de hijos. Su contenido se alinea vertical y horizontalmente respectivamente.

En ambos casos, el tamaño de estos widgets es el 100% del tamaño del Widget que los envuelve. En estos casos de tamaño variable, si queremos ponerles un child de un elemento cuyo tamaño se ajuste al padre, como el padre tiene un tamaño variable, no se verán. Para solucionar el problema, usaremos el Widget Expanded, que toma el tamaño del Widget padre.

Este código funciona, los TextField se visualizan

child: Row(
  children: [
    Expanded(child: TextField()),
    Expanded(child: TextField()),

  ],
),

Este código no funciona, los TextField no se visualizan:

child: Row(
  children: [
    TextField(),
    TextField(),
  ],
),

Atributos importantes:

Alinea todo el contenido según el valor recibido.

crossAxisAlignment: CrossAxisAlignment.start,

Poner un borde redondeado:

 decoration: BoxDecoration(
                  color: Colors.yellow,
                  borderRadius: BorderRadius.circular(12)),

Debes tener en cuenta que si estamos utilizando decoration debemos ponerle un color al panel.

Center

Este widget sólo acepta un único hijo (child). Centra su contenido horizontal y vericalmente.

Align

Este widget permite alinear su contenido a derecha, izquierda, arriba o abajo.

Expanded

Es un widget que hace que su contenido se estire todo lo posible.

Sizedbox

Permite poner separaciones entre widgets. Es similar a lo que haríamos si usásemos paddings o margins.

SizedBox(
  width: 16,
),

Padding

Nos permite añadir padding a los elementos.

Padding(
  padding: const EdgeInsets.all(12.0),
  child: ...
),

El parámetro padding acepta varios valores:

padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),


padding: const EdgeInsets.only(right: 30),

SingleChildScrollView

Para que nuestro contenido tenga Scroll. Puede envolver cualquier Widget.

A menudo la columna de nuestro contenido deberá estar dentro de un SingleChildScrollView, ya que al pulsar sobre un cuadro de texto la pantalla se comprimirá para mostrar el teclado y no nos quedará espacio.

GradientBack

Es una especie de div con degradado.

CardImageList

Es un carrusel de elementos.

ListView 

Muestra un listado de elementos

Checkbox

Radio

El value de un radio puede ser de cualquier tipo, no sólo de tipo String.

Para que la opción marcada en un radio button se visualice, habrá que repintar la pantalla llamando al método setState.

La propiedad groupValue tiene el valor escogido de los 3. Cuando groupValue sea igual a value, el radio se marcará.

Radio(
  autofocus: true,
  value: 'F',
  groupValue: sexType,
  onChanged: (String? value) => onSexChange(value)),

...

  onSexChange(String? nuevoValor) {
    setState(() {
      sexType = nuevoValor;
    });
  }

Slider

Form

DatePicker

DateTime selectedDate = DateTime.now();
...
_selectDate(BuildContext context) async {
  final DateTime picked = await showDatePicker(
    context: context,
    initialDate: selectedDate, // Refer step 1
    firstDate: DateTime(2000),
    lastDate: DateTime(2025),
  );
  if (picked != null && picked != selectedDate)
    setState(() {
      selectedDate = picked;
    });
}

Botones

OutlinedButton(onPressed: onPressed, child:Text('hola')),

TextButton(onPressed: onPressed, child: Text('hola')),

ElevatedButton(onPressed: onPressed, child: Text('Hola'))

IconButton(
 icon: const Icon(Icons.check),
 color: Colors.white,
 onPressed: () {},
 iconSize: 30,
)

Cargar botón con imagen

TextButton(
  onPressed: () => updateCheckPoint(index),
  child: Image.asset(
    'assets/checked.png',
    width: 30,
    fit: BoxFit.cover,
  ),
 )

GestureDetector e InkWell

Nos permiten detectar eventos de todo tipo sobre su contenido (tap, doble tap, drag).

GestureDetector(
 onTap: () => setState(() => sexType = 'M'),
 child: Text('Masculino')),

El InkWell tiene animaciones cuando interactuamos con él, pero soporta un menor tipo de eventos.

InkWell(
   onTap: () => setState(() => sexType = 'M'),
   child: MonteserinText('Masculino')),

Renderizado condicional

widgets...
if (condicion)
    Boton(...),
widgets...

El if debe hacerse sin llaves.

widgets...
condicion ? boton1() : boton2(),
widgets...
widgets...
Visibility(visible: condicion, child: boton(...)),
widgets...

Asincronía en Flutter

Cuando un método devuelve Future<loQueSea>, ese método es asíncrono.

No sabemos cuando un método asíncrono va a concluir.

Los métodos asíncronos suelen tener el modificador async:

Future<String> loadString(String cosas) async {

Para procesar un Future, tenemos dos opciones.

  • Usar await, que detendrá la ejecución del código hasta que haya concluído el método.
final String data = await rootBundle.loadString('assets/json/data.json');

El modificador await convierte a la función que lo envuelve en asíncrona, y por tanto tendremos que añadirle el modificador async. Tiene sentido porque cuando detenemos la ejecución de una función hasta que no haya concluído cierto método, la estamos convirtiendo en asíncrona.

  • Usar then:
final resultado =  rootBundle.loadString('assets/json/data.json');
    resultado.then((value) {

}).catchError((error) {

}).whenComplete(() {

});

Cargar una tipografía

pubspec.yml
flutter:
  fonts:
    - family: kravitz
      fonts:
        - asset: assets/fonts/kravitz.ttf
    - family: routhem
      fonts:
        - asset: assets/fonts/routhem.ttf
main.dart
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        fontFamily: 'kravitz'
      ),
Text('Hola',
  style: TextStyle(font-fontFamily: 'routhem'),
),

Leer un JSON con Flutter

pubspec.yaml

flutter:
  

  uses-material-design: true
  assets:
    - assets/json/
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;

List items = [];

  
Future<void> readJson() async {
  final String response = await rootBundle.loadString('assets/data.json');
  items = await jsonDecode(response);

   setState(() {

   });
}

Pintar los datos en pantalla

Expanded(
  child: ListView.builder(
    
itemCount: items.length,
    itemBuilder: (context, index) {
      final item = items[index];

        return Card(
          color: Colors.green[100],
          child: ListTile(
          title: Text(item["name"]),//item.,
        subtitle: Text(item["days"].toString()),//item.,

         ),
      );
    },
  ),
),

Importando un Widget definido en un fichero externo:

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


class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        ...
        home: Scaffold(
            ...
            body: new Widget1(),
        )
    );
  }
}

widget_1.dart

import 'package:flutter/material.dart';

class  Widget1 extends StatelessWidget {
  const Widget1();

  @override
  Widget build(BuildContext context) {
    final myRow = Row(
      children: [
        Container(
            width: 50,
            height: 50,
            color:Colors.pink
        ),
        Container(
            width: 50,
            height: 50,
            color:Colors.blue
        )
      ],
    );
    return myRow;
  }
}

Mostrando el contenido de un array con un ListView

body:  ListView.builder(
  itemCount: names.length,
  itemBuilder: (BuildContext context, int index) {
    return Container(
      height: 50,
      margin: EdgeInsets.all(2),
      color: Colors.green,
      child: Center(
        child: Text(
          ${names[index]}',
          style: TextStyle(fontSize: 22, color: Colors.white),
        )
      ),
    );
  }
)

Llamadas a funciones desde un Widget

El siguiente código es correcto:

OutlinedButton(onPressed: calculate, child: Text('Calcular'))

...

calculate()

Una función sin paréntesis es un puntero a dicha función y podrá ser asignada a un parámetro de tipo función.

Curso de Flutter 3

Pero este código no es correcto:

OutlinedButton(onPressed: calculate(), child: Text('Calcular'))

En el código anterior, la función calculate se está llamando en el momento de crear el Widget, en lugar de cuando presionemos sobre él. Lo correcto sería haber hecho:

OutlinedButton(onPressed: () => calculate(), child: Text('Calcular'))

Llamar a una función del padre desde un widget del hijo

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

class MonteserinDropDownButton extends StatelessWidget {
  final Function(String?)  onChanged;

  MonteserinDropDownButton(this.items, this.val, this.onChanged,);

  @override
  Widget build(BuildContext context) {
    return DropdownButton(
      ...
      onChanged: (valor)=> onChanged(valor),
    );

  }
}

initState()

Este método se ejecuta una sola vez cuando se crea el componente y suele utilizarse para inicializar variables miembro que forman parte del estado de nuestro stateFlulWidget.

Sólo está disponible en los stateFulWidget.

Este método se ejecuta antes de renderizar el componente. Mucho cuidado con llamar a métodos asíncronos desde él, ya que entonces el componente se renderizará antes de que el estado se inicialice y dará error.

late List items;
  
void initState() {
     getItems();
}

getItems() async{
    items = jsonDecode(itemsRecovered);
}

Hacer un botón con estilo personalizado

Formularios

  • Para recoger y procesar datos introducidos por el usuario, los elementos del formulario deberían estar dentro de un StateFulWidget (En nuestro caso, HomePage).
  • Para recoger valores de los Textfield, utilizaremos la propiedad controller.
  • Las variables n1 y n2 que almacenarán los valores introducidos por el usuario serán declarados como variables globales.
  • El constructor de un StateFulWidget se ejecuta cada vez que este se repinta. En nuestro caso, no será necesario inicializar las variables n1 y n2 cada vez que la aplicación se repinta, y por eso para inicializarlas hemos usado el método initState.
  • Después de calcular el resultado, en el método result, debemos repintar para que este se visualice en pantalla. Para ello llamámos al método setState;
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late TextEditingController n1, n2;
  String result = '';

  @override
  void initState() {
    super.initState();
    n1 = new TextEditingController();
    n2 = new TextEditingController();
  }


  onPressed() {
    print('n1: ${n1.text} n2: ${n2.text}' );
    final r = int.parse(n1.text) + int.parse(n2.text);
    result = r.toString();
    setState(() {

    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          children: [
            Expanded(child: TextField(controller:n1, keyboardType: TextInputType.number)),
            SizedBox(
              width: 16,
            ),
            Expanded(child: TextField(controller: n2,keyboardType: TextInputType.number,)),
            Expanded(child: Text(result)),
            OutlinedButton(onPressed: onPressed, child: Text('Enviar'))
          ],
        ),
      ),
    );
  }
}
Descargar Recurso

Añadir un DatePicker

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

...  

  selectDate(BuildContext context) async {
    final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: selectedDate, // Refer step 1
      firstDate: DateTime(2000),
      lastDate: DateTime(2025),
    );
    if (picked != null && picked != selectedDate) {
      setState(() {
        selectedDate = picked;
      });
    }
  }

...

OutlinedButton(onPressed: () => selectDate(context), child: Text('Fecha de nacimiento')),

Creación de nuestros propios Widgets

Widgets sin estado (StateLessWidget)

El snippet para codificarlos es: stless

El usuario no va a interactuar directamente con ellos.

Widgets con estado (StatefulWidget)

Son aquellos con los que el usuario puede interactuar directamente.

El snippet para codificarlos es: stful

Gestión de nulos

Como recomendación, trataremos de inicializar las variables miembro en su declaración, en lugar de usar el modificador late o el operador ?.

Modificador late

Nos permite declarar una variable a la que luego debemos asignarle un valor concreto. Si intentamos acceder a una variable late a la que no le hemos asignado un valor, tendremos un error.:

late TextEditingController textEditingControllerSex;

late se suele utilizar para variables inicializadas en el initState. Este es el ejemplo ideal:

  @override
  void initState() {
    textEditingControllerSex = TextEditingController();
  }

Una variable late no es que valga null, es que no está inicializada.

Operador ?.

Este operador significa: si el objeto es nulo, no hagas esto.

textEditingControllerSex?.dispose();

En el ejemplo anterior, en el caso de que textEditingControllerSex sea nulo, no se ejecutrará el método dispose().

Operador ?

Para declarar una variable que pueda tener valores nulos, debemos utilizar la ? después del tipo.

String? sexType;

Existen funciones que devuelven objetos que pueden ser null. En estos casos, debemos utilizar el operador ? a continuación del tipo de la variable que almacena el valor. Este operador, indica que esa variable almacena un valor que puede ser nulo..

final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: selectedDate, // Refer step 1
      firstDate: DateTime(2000),
      lastDate: DateTime(2025),
);

Operador ??

En ocasiones, al asignar un valor a una variable, debemos asegurarnos de que el valor asignado no es nulo. Podemos hacer esto utilizando el operador ?? que asignará el valor indicado en el caso de que el valor que se iba a asignar primeramente sea nulo.

sexType = nuevoValor ?? '';

Navegar a otra pantalla

Navigator.push(
   context,
   MaterialPageRoute(builder: (context) => SecondPage()),
);

Cerrar una pantalla abierta

Navigator.pop(context);

Cerrar una pantalla abierta pasando información a la pantalla a la que regreso

Navigator.pop(context, true);

Recoger parámetros de una pantalla que cerramos

Es fundamental utilizar e modificador await, que esperará a que cerremos la pantalla.

openScreen() async {
    final otherScreenPassedValue = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => AddItem()),
    );

    if ( otherScreenPassedValue != null && otherScreenPassedValue as bool) {
      getItems();
    }
}

Pasando parámetros

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => SecondPage(
      valorAPasar: valorAPasar)
  ),
);
class SecondPage extends StatefulWidget {
  final valorAPasar;
  ...
  @override
  _EditItemState createState() => _EditItemState();
}

class _SecondPage extends State<EditItem> {
  void initState(){
    print(widget.valorAPasar)
  }

Añadir publicidad

1. Registramos un cuenta de Google Ads en el caso de no tenerla y accedemos a https://apps.admob.com.

2. Desde la página de admob, creamos una nueva App: App → Add App. Seguimos los pasos que se nos indican.

3. Llegaremos a una página en la que se nos propondrá crear un nuevo Unit (una nueva publicidad). Escogemos el banner.

Curso de Flutter 4

4. En el caso de una publicidad para Android, debemos copiar el App Id de nuestra aplicación (disponible en Apps → App Settings) en un nuevo nodo meta-data (en la propiedad android:value) del fichero manifest.

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.percentiles.percentiles">
   <application
        android:label="percentiles"
        android:icon="@mipmap/ic_launcher">

       <meta-datap
           android:name="com.google.android.gms.ads.APPLICATION_ID"
           android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>

Un error muy típico consiste en no meter el meta-data dentro del nodo application. Para evitarlo, meteremos el meta-data inmediatamente dentro y al principio del nodo application.

5. Cargamos el módulo de publicidad:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  google_mobile_ads: ^0.13.4

Después de modificar los ficheros gradle o el manifest de Android hay que hacer un ToolsFlutterClean. Y luego un pub get.

6. Inicializamos los anuncios en el main de nuestra aplicación:

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();

  runApp(const MyApp());
}

6. En la página donde querramos mostrar el banner:

import 'package:google_mobile_ads/google_mobile_ads.dart';


late BannerAd _bannerAd;
...

    _bannerAd = BannerAd(
      //   adUnitId: 'ca-app-pub-3340658405798791/3405324824' ,
      adUnitId: 'ca-app-pub-3940256099942544/6300978111', // PARA PRUEBAS //
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener()
    );

    _bannerAd.load();

// en el widget tree. Los widgets de anuncio necesita que le indiquemos la altura que van a ocupar.:

 Container(height: 100,child: AdWidget(ad: _bannerAd)),

Para probar los banners usaremos un adUnitId de pruebas. Luego, si todo va bien, lo sustituiremos por el que hemos obtenido en la web de admob.

https://codelabs.developers.google.com/codelabs/admob-ads-in-flutter#0

Crear un Widget reutilizable a partir de otros Widgets:

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

class MonteserinTitle extends StatelessWidget {

  final String _texto;


  MonteserinTitle(this._texto);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
          color: Colors.yellow,
          borderRadius: BorderRadius.circular(12)),
      child: Text(_texto,
        style: TextStyle( fontSize: 40),
      ),
    );
  }
}

Llamar a una función al cabo de un tiempo, incluso en segundo plano

Lo métodos de Timer se ejecutarán incluso en segundo plano.

class _MyHomePageState extends State<MyHomePage> {
  int _secondsSelect = 10;
  static const int INCR = 5;
  bool _cronoActive = false;
  int _secondsRemain = 0;
  Timer? timer;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (!_cronoActive)
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                      onPressed: () {
                        if (_secondsSelect - INCR > 0)
                          setState(() {
                            _secondsSelect -= INCR;
                          });
                      },
                      icon: Icon(Icons.chevron_left)),
                  Text("$_secondsSelect Segundos"),
                  IconButton(
                      onPressed: () {
                        setState(() {
                          _secondsSelect += INCR;
                        });
                      },
                      icon: Icon(Icons.chevron_right))
                ],
              ),
            ElevatedButton(
                onPressed: _cronoActive ? _stopCrono : _startCrono, child: Text(_cronoActive ? "Detener" : "Activar")),

            if (_cronoActive) Text(_secondsRemain.toString())
          ],
        ),
      ),
    );
  }

  _startCrono() {
    setState(() {
      _cronoActive = true;
      _secondsRemain = _secondsSelect;
      timer = Timer.periodic(const Duration(seconds: 1), (Timer t) => _updateProgress());
    });
  }

  _updateProgress() {
    setState(() {
      _secondsRemain--;
      if (_secondsRemain <= 0) {
        FlutterBeep.beep();
        _stopCrono();
        _startCrono();
      }
    });
  }

  _stopCrono() {
    setState(() {
      timer!.cancel();
      _cronoActive = false;
    });
  }
}

Flame

Es un motor de videojuegos 2D para Flutter.

dependencies:
  flutter:
    sdk: flutter
  flame: 0.29.4
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyGame().widget);

class MyGame extends BaseGame {
  @override
  void render(Canvas canvas) {

  }

  @override
  void update(double t) {
    
  }
}

Internacionalización

1. Cargamos las dependencias de internacionalización

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations: # Add this line
    sdk: flutter         # Add this line
  intl: ^0.17.0 # Add this line

# The following section is specific to Flutter.
flutter:
  generate: true # Add this line

2. Creamos un nuevo fichero:

l10n.yaml

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

3. ${FLUTTER_PROJECT}/lib/l10n/app_en.arb

{
    "helloWorld": "Hello World!",
    "@helloWorld": {
      "description": "The conventional newborn programmer greeting"
    }
}

4. Definimos las dependencias para nuestra Material App:

import 'package:flutter_localizations/flutter_localizations.dart';
return const MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: AppLocalizations.supportedLocales,
  home: MyHomePage(),
);

5. Para usar la localización:

import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Text(
  AppLocalizations.of(context)!.empty),
),

Cambiar el icono de la aplicación

1. Abriremos la aplicación con Android Studio ( Menú ToolsFlutter Open Android Module in Android Studio).

2. Pulsamos con el botón derecho sobre alguno de las carpetas de la aplicación → NewImage Asset.

3. Cargamos la imagen de la aplicación. Aunque inicialmente hay cargado un fichero xml, no habrá problema en cargar una imagen.

Publicación

  1. Generación del fichero keystore. Menú Tools → Flutter → Open Android module in Android Studio → Menú Build de la nueva interfaz de Android Studio que se abre → Generate Signed Bundle / APK
  2. Modicamos los ficheros necesarios dentro de nuestro proyecto de Flutter para hacer el build. Tienes la documentación oficial de como hacerlo en este enlace. Puedes seguir la guía a partir del paso Reference the keystore from the app.
  3. Cuando esté todo configurado pulsamos el menú BuildFlutterBuild App Bundle.