Tipo de dato Function vs VoidCalback

En los lenguajes de programación tipados es importante hacer uso correcto de cada tipo, por seguridad, por rendimiento, por buenas prácticas  etc.

Una de esas cosas que  di  por sentado aplicando la  mítica frase "igual funciona" fue la declaración de parámetros con Fuction y VoidCallback, siempre usando Function, pero vamos a detallar cada uno, claro y al grano.


VoidCallback 

Es el tipo de función especifico de Function() que no recibe ni retorna nada (es el nombre abreviado para void Function() ), es ideal usarlo como su mismo nombre lo dice, callback, cuando solo necesitamos ejecutar una acción, una notificación simple de un hijo a un padre.

Tomando en cuenta lo anteriormente dicho veamos este ejemplo...


class MiBoton extends StatelessWidget {
  final VoidCallback alPresionar; // No recibe ni devuelve nada

  const MiBoton({required this.alPresionar});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
     onPressed: alPresionar, //simplemente notifica que algo pasó  
     child: Text('Presionar'));
  }
}

Y desde el padre...


class Padre extends StatefulWidget {
  @override
  _PadreState createState() => _PadreState();
}

class _PadreState extends State {
  

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Come frutas y verduras")),
      body: MiBoton(
        alPresionar: () => print('Precionando ando'),
      ) ,
    );
  }
}


Function()

Es la clase abstracta de todas las funciones, y aqui es donde viene la mala práctica, declarar un parámetro solo con Function el compilador de Dart dejara de verificar el tipado y dejará pasar cualquier tipo, aumentando el riesgo de error de tipo en tiempos de ejecución. 

Tomando como base el ejemplo anterior modifiquemos la clase MiBoton para que reciba un parametro de tipo Function que recibe un String...


class MiBoton extends StatelessWidget {
  final void Function(String) alPresionar; //Function recibe un parametro de tipo String

  const MiBoton({required this.alPresionar});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            alPresionar('Boton 1 presionado');
          },
          child: Text('Presionar boton 1'),
        ),
        ElevatedButton(
          onPressed: () {
            alPresionar('Boton 2 presionado');
          },
          child: Text('Presionar boton 2'),
        ),
      ],
    );
  }
}

Y el padre...


class Padre extends StatefulWidget {
  @override
  _PadreState createState() => _PadreState();
}

class _PadreState extends State {
  

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Obedece a tus padres")),
      body: MiBoton(
        alPresionar: (String value) => print(value),
      ) ,
    );
  }
}

Un ejemplo burdo pero sirve para mostrar como se pasa el valor del hijo al padre. El padre para poder llamar la clase MiBoton debe pasar al parámetro llamado alPresionar una función que recibe un String como parámetro; Lo que haga esa función con el parámetro la clase MiBoton  no lo sabe y tampoco le importa, lo que le importa es que ese pase de parámetro exista.

Otro caso es cuando el parámetro de tipo Function() a parte de recibir un parámetro   también retorna un valor, pero ese caso lo dejamos para otra  ocasión.