Flutter DART Funciones
Sumario
Introducción
- En Dart, las funciones permiten agrupar código que puede ejecutarse desde diferentes partes de un programa.
: Como este curso está orientado a alumnado que ya sabe programar en algún lenguaje, no profundizaremos en los conceptos generales de las funciones, sino en las características específicas que ofrece Dart.
- Es recomendable indicar el tipo de los parámetros que recibe una función y el tipo del dato que devuelve.
- Veamos un ejemplo de función con parámetros:
double sumarValores(double val1, double val2) { return val1 + val2; } void main() { final num1 = 10.32; final num2 = 1.35; print(sumarValores(num1, num2)); }
Paso de parámetros
- Al igual que en otros lenguajes, podemos pasar datos a las funciones empleando parámetros.
- Como ya vimos anteriormente, podemos definir parámetros que admitan valores nulos utilizando los tipos nullable, como por ejemplo
String?,int?odouble?.
Paso por valor
- En Dart, los argumentos de una función se pasan por valor.
- Las variables de Dart contienen referencias a objetos. Cuando enviamos una variable como argumento de una función, se copia el valor almacenado en dicha variable.
- Si dicho valor es una referencia a un objeto, se copia la referencia. Por tanto, tanto la variable original como el parámetro pueden hacer referencia al mismo objeto.
- Esto provoca dos comportamientos que debemos diferenciar:
- Si reasignamos el parámetro dentro de la función, no modificamos la variable utilizada en la llamada.
- Si utilizamos la referencia recibida para modificar el estado de un objeto mutable, el cambio será visible fuera de la función, ya que ambas referencias permiten acceder al mismo objeto.
- </syntaxhighlight>
:<syntaxhighlight lang="dart" enclose="div"> class Pelota { double? peso; String? id; } void modificarValores( Pelota pelota, int valor, String cadena, ) { valor = 10; cadena = 'NUEVA CADENA'; pelota.peso = 100.2; pelota.id = 'P2'; } void main() { var valorPrueba = 5; var cadena = 'VIEJA CADENA'; final pelota = Pelota(); pelota.id = 'P1'; pelota.peso = 50.34; modificarValores( pelota, valorPrueba, cadena, ); print('Valor numérico: $valorPrueba'); print('Cadena: $cadena'); print('Objeto: ${pelota.id} -- ${pelota.peso}'); }
- Después de ejecutar la función:
valorPruebasigue valiendo5.cadenasigue conteniendo'VIEJA CADENA'.- Las propiedades del objeto
pelotasí han cambiado.
- Por tanto, no es correcto decir que Dart pasa unos tipos por valor y otros por referencia. Todos los argumentos se pasan por valor; cuando trabajamos con objetos, el valor copiado es una referencia al objeto.
Parámetros opcionales posicionales
- Podemos definir parámetros posicionales opcionales escribiéndolos entre corchetes
[ ].
void imprimirMensaje([String mensaje = 'Nada enviado']) { print(mensaje); } void main() { imprimirMensaje(); imprimirMensaje('Nuevo mensaje'); }
- En la primera llamada no enviamos ningún valor, por lo que el parámetro utiliza su valor por defecto.
- Si el parámetro admite valores nulos, no es necesario establecer un valor por defecto:
void imprimirMensaje([String? mensaje]) { print(mensaje); } void main() { imprimirMensaje(); imprimirMensaje('Nuevo mensaje'); }
- Si no enviamos ningún valor,
mensajetendrá el valornull.
- Si una función tiene parámetros posicionales obligatorios y opcionales, los opcionales deben aparecer al final:
void imprimirMensaje(int numero, [String? mensaje]) { print('$numero - $mensaje'); }
Parámetros con nombre
- Dart permite definir parámetros con nombre.
- Para ello se escriben los parámetros entre llaves
{ }.
void imprimirMensaje({ String? mensaje, }) { print(mensaje); }
- Al realizar la llamada indicamos el nombre del parámetro:
void main() { imprimirMensaje( mensaje: 'Hola', ); }
- Una ventaja de los parámetros con nombre es que podemos indicar los argumentos por su nombre, haciendo las llamadas más fáciles de leer.
Parámetros con nombre opcionales
- Los parámetros con nombre son opcionales por defecto.
- Si el parámetro puede valer
null, podemos declararlo como nullable:
void imprimirMensaje1({ String? mensaje, }) { print(mensaje); } void main() { imprimirMensaje1(); imprimirMensaje1( mensaje: 'Hola', ); }
- Otra posibilidad es proporcionar un valor por defecto:
void imprimirMensaje2({ String mensaje = 'Valor por defecto', }) { print(mensaje); } void main() { imprimirMensaje2(); imprimirMensaje2( mensaje: 'Nuevo mensaje', ); }
Parámetros con nombre obligatorios
- Podemos indicar que un parámetro con nombre es obligatorio utilizando la palabra clave
required.
void imprimirMensaje({ required String mensaje, }) { print(mensaje); } void main() { imprimirMensaje( mensaje: 'Mensaje', ); }
- En este caso estamos obligados a enviar el argumento correspondiente a
mensaje.
- Como el parámetro es obligatorio y su tipo es
String, no necesitamos asignarle un valor por defecto ni permitir que seanull.
Combinación de parámetros posicionales y con nombre
- Podemos combinar parámetros posicionales y parámetros con nombre.
- Los parámetros posicionales se escriben antes que los parámetros con nombre:
void imprimirMensaje( int numero, { String? mensaje, }) { print( 'El número es $numero y el mensaje es $mensaje', ); } void main() { imprimirMensaje( 10, mensaje: 'Mensaje', ); }
Funciones flecha o arrow functions
- Cuando una función está formada por una única expresión, podemos utilizar la sintaxis de función flecha mediante el operador
=>.
- Por ejemplo, la siguiente función:
double sumar(double a, double b) { return a + b; }
- puede escribirse como:
double sumar(double a, double b) => a + b;
- Después de
=>se escribe una expresión.
- El resultado de dicha expresión será el valor devuelto por la función cuando esta tenga un tipo de retorno distinto de
void.
double calcularMedia( double num1, double num2, ) => (num1 + num2) / 2;
- También podemos emplearla con una expresión que no produzca un valor útil, como una llamada a
print:
void imprimirMediaNumero({ required int num1, required int num2, }) => print((num1 + num2) / 2); void main() { imprimirMediaNumero( num1: 10, num2: 15, ); }
- Si necesitamos ejecutar varias instrucciones, debemos emplear un bloque de código con llaves:
void mostrarDatos(int numero) { print('Número recibido:'); print(numero); }
Funciones anónimas
- Una función anónima es una función que no tiene un nombre asociado.
- Las funciones anónimas son especialmente útiles como argumentos de métodos que esperan recibir otra función.
- Este tipo de métodos aparece frecuentemente cuando trabajamos con colecciones y también en Flutter mediante callbacks.
- Una función anónima puede tener un bloque de código:
(elemento) { print(elemento); }
- Si únicamente necesitamos una expresión, podemos emplear la sintaxis flecha:
(elemento) => print(elemento)
Funciones anónimas y forEach
- El método
forEachde unIterableespera recibir una función que será ejecutada una vez por cada elemento.
- Por ejemplo:
void main() { final lista = <int>[1, 2, 3, 4, 5, 6]; lista.forEach((elemento) { print('Accediendo al elemento $elemento'); }); }
- El parámetro
elementorepresenta, en cada ejecución, uno de los elementos de la lista.
- Podemos expresar lo mismo mediante una función flecha:
void main() { final lista = <int>[1, 2, 3, 4, 5, 6]; lista.forEach( (elemento) => print( 'Accediendo al elemento $elemento', ), ); }
Nota importante:
- Aunque estas formas son válidas y sirven para comprender el funcionamiento de las funciones anónimas, para realizar un recorrido sencillo de un
Iterable, Dart recomienda normalmente utilizarfor...inen lugar deforEachcon una función anónima.
- Por ejemplo:
void main() { final lista = <int>[1, 2, 3, 4, 5, 6]; for (final elemento in lista) { print( 'Accediendo al elemento $elemento', ); } }
Pasar una función existente como parámetro
- Cuando un método espera una función como parámetro, no siempre tenemos que crear una función anónima.
- Podemos pasar una función que ya esté definida:
void recorrerConjunto(String elemento) { print(elemento); } void main() { final conjunto = <String>{ 'uno', 'dos', 'tres', }; conjunto.forEach(recorrerConjunto); }
- Fijarse en que escribimos:
conjunto.forEach(recorrerConjunto);
- y no:
conjunto.forEach(recorrerConjunto());
- No queremos ejecutar
recorrerConjuntoen ese momento. Queremos proporcionar la función aforEachpara que sea este método quien la ejecute posteriormente una vez por cada elemento.
- La función que enviemos debe ser compatible con el tipo de función que espera el método.
- En este ejemplo, el conjunto contiene elementos de tipo
String, por lo que la función recibe unStringy devuelvevoid:
void recorrerConjunto(String elemento) { print(elemento); }
Tear-offs
- Cuando hacemos referencia a una función o método sin utilizar los paréntesis de llamada, Dart crea lo que se denomina un tear-off.
- Por ejemplo:
conjunto.forEach(recorrerConjunto);
recorrerConjuntorepresenta la función que queremos proporcionar aforEach.
- Dart recomienda no crear una función anónima innecesaria cuando podemos pasar directamente una función existente.
- Por tanto, en lugar de:
lista.forEach( (elemento) => gestionarElemento(elemento), );
- es preferible:
lista.forEach(gestionarElemento);
- Incluso podemos proporcionar directamente una función existente de Dart si es compatible con los parámetros esperados:
lista.forEach(print);
Diferentes formas de proporcionar una función a forEach
- Veamos las diferentes posibilidades:
void gestionarElemento(int elemento) { print('Elemento: $elemento'); } void main() { final lista = <int>[1, 2, 3, 4, 5, 6]; // Función anónima con bloque. lista.forEach((elemento) { print('Función anónima: $elemento'); }); // Función anónima flecha. lista.forEach( (elemento) => print('Función flecha: $elemento'), ); // Función previamente definida. lista.forEach(gestionarElemento); // Función existente compatible. lista.forEach(print); }
- Las dos primeras formas son útiles para comprender cómo funcionan las funciones anónimas.
- Sin embargo, si únicamente queremos recorrer un
Iterable, normalmente emplearemos:
for (final elemento in lista) { print(elemento); }
- Si queremos ejecutar exactamente una función existente para cada elemento,
forEaches apropiado:
lista.forEach(gestionarElemento); lista.forEach(print);
Funciones que devuelven funciones
- En Dart, las funciones son objetos. Esto significa que una función también puede devolver otra función.
- Veamos un ejemplo:
void Function(int) crearFuncion() { return (int elemento) { print('Número: $elemento'); }; } void main() { final lista = <int>[1, 2, 3]; lista.forEach(crearFuncion()); }
- En este caso ocurre algo diferente al ejemplo anterior.
- Primero se ejecuta:
crearFuncion()
- Esta función devuelve otra función compatible con la función que espera
forEach.
- Por ese motivo, en este caso sí aparecen los paréntesis después de
crearFuncion.
- Este tipo de funciones recibe el nombre de funciones de orden superior. También están relacionadas con el concepto de closures, que veremos cuando sea necesario.
forEach en mapas
- Los objetos de tipo
Maptambién disponen del métodoforEach.
- En este caso, la función recibe dos parámetros:
- La clave.
- El valor asociado a dicha clave.
void main() { final mapa = <String, double>{ 'base': 1.84, 'alero': 1.95, 'pivot': 2.10, 'escolta': 1.90, }; mapa.forEach((clave, valor) { print( 'La clave $clave tiene el valor $valor', ); }); }
- En el caso de
Map.forEach, el empleo de una función anónima es totalmente apropiado.
- Si queremos recorrer solamente las claves o solamente los valores, podemos emplear
for...in:
void main() { final mapa = <String, double>{ 'base': 1.84, 'alero': 1.95, 'pivot': 2.10, 'escolta': 1.90, }; for (final clave in mapa.keys) { print('Clave: $clave'); } for (final valor in mapa.values) { print('Valor: $valor'); } }
Recorrido de conjuntos
- Los conjuntos (
Set) son objetosIterable, por lo que para un recorrido sencillo podemos utilizarfor...in:
void main() { final conjunto = <String>{ 'Ángel', 'Luis', 'Ana', 'María', }; for (final elemento in conjunto) { print(elemento); } }
- También podemos utilizar
forEachsi proporcionamos directamente una función existente:
void main() { final conjunto = <String>{ 'Ángel', 'Luis', 'Ana', 'María', }; conjunto.forEach(print); }
Método where
- El método
wherepermite filtrar los elementos de unIterable.
- Recibe como parámetro una función que debe devolver un valor booleano:
(elemento) => condición
- Si la función devuelve
true, el elemento formará parte del resultado.
- Si devuelve
false, el elemento será descartado.
- Por ejemplo:
void main() { final numeros = <int>[ 1, 2, 3, 4, 5, 6, ]; final pares = numeros.where( (numero) => numero.isEven, ); print(pares); }
wheredevuelve unIterable.
- Si queremos obtener una lista:
final pares = numeros .where((numero) => numero.isEven) .toList();
- Si queremos obtener un conjunto:
final pares = numeros .where((numero) => numero.isEven) .toSet();
Método every
- El método
everypermite comprobar si todos los elementos de unIterablecumplen una determinada condición.
- Devuelve un valor de tipo
bool.
void main() { final numeros = <int>[ 10, 20, 30, 40, ]; final todosPositivos = numeros.every( (numero) => numero > 0, ); print(todosPositivos); }
- El resultado será
trueúnicamente si todos los elementos cumplen la condición.
Método map aplicado a List y Set
- El método
mappermite transformar los elementos de unIterable.
- Por cada elemento de la colección original se ejecuta una función que genera un nuevo valor.
- Como resultado obtenemos un nuevo
Iterable.
- Por ejemplo:
void main() { final numeros = <int>[ 1, 2, 3, 4, ]; final cuadrados = numeros.map( (numero) => numero * numero, ); print(cuadrados); }
- Habitualmente convertiremos el resultado a una lista mediante
toList():
final cuadrados = numeros .map((numero) => numero * numero) .toList();
- Este método es especialmente útil en Flutter, ya que permite crear una colección de widgets a partir de una colección de datos.
- Por ejemplo, conceptualmente podemos transformar una lista de cadenas en una lista de objetos:
void main() { final nombres = <String>[ 'Ana', 'Luis', 'María', ]; final mensajes = nombres .map((nombre) => 'Hola $nombre') .toList(); print(mensajes); }
- También podemos aplicar
mapsobre las entradas de unMap:
void main() { final mapa = <String, String>{ 'nombre': 'Ángel', 'apellido1': 'Fernández', 'apellido2': 'González', }; final lista = mapa.entries .map( (entrada) => 'HOLA: ${entrada.value}', ) .toList(); print(lista); }
List.generate
- La clase
Listdispone del constructorList.generate, que permite generar los elementos de una lista mediante una función.
- La función recibe la posición que se está generando:
void main() { final numeros = List<int>.generate( 5, (posicion) => posicion, ); print(numeros); }
- El resultado será:
[0, 1, 2, 3, 4]
- Si queremos generar los números desde 1:
final numeros = List<int>.generate( 5, (posicion) => posicion + 1, );
- Podemos indicar mediante
growable: falseque la lista tenga longitud fija:
final numeros = List<int>.generate( 5, (posicion) => posicion + 1, growable: false, );
Importante:
- Una lista con
growable: falsees una lista de longitud fija. Esto significa que no podemos añadir ni eliminar elementos.
- Sin embargo, esto no significa que la lista sea inmutable. Podemos modificar el valor almacenado en una posición existente.
Ejercicios propuestos
Ejercicio 1:
Crea una función que espere recibir mediante parámetros con nombre una posición y una lista de cadenas, y devuelva el valor de la lista que se encuentra en la posición indicada.
- La lista debe ser enviada obligatoriamente.
- Si no se envía la posición, debe utilizarse la posición
0. - Si la lista no tiene elementos, debe devolver la cadena
'Sin datos'. - Si la posición es negativa o no existe en la lista, debe devolver la cadena
'Índice fuera de rango'. - Llama a la función empleando diferentes valores para comprobar su funcionamiento.
Ejercicio 2:
Crea un mapa de datos en el que se guarde como clave un índice entero y como valor un precio.
- Define inicialmente cuatro entradas en el mapa.
- Añade dos nuevas entradas.
- Borra la última entrada introducida.
- Recorre el mapa empleando
forEach. La función debe mostrar la clave y el precio de todos los platos cuyo precio sea inferior a 5 euros. - Crea una función de nombre
getNumPlatosMasCarosa la que se le pase mediante parámetros con nombre un mapa y un precio. - Los dos parámetros deben ser obligatorios y no admitir valores nulos.
- La función debe devolver cuántos platos tienen un precio superior al indicado.
- Emplea
wherepara realizar el filtrado. - Llama a la función para comprobar que funciona.
Ejercicio 3:
Crea un conjunto de nombre equipo que contenga los valores:
Base Alero Pivot Escolta
Crea una función que reciba como parámetros el conjunto y una cadena a buscar.
- Emplea el método
where. - La función debe mostrar los elementos del conjunto que contengan la cadena indicada.
- Recorre el resultado empleando
for...in. - Realiza una búsqueda con la cadena
'as'. - Realiza otra búsqueda con la cadena
's'.
- Emplea el método
Ejercicio 4:
Define una lista de números decimales con los siguientes valores:
10.0, 11.22, 15.0, 7.0, 3.44
Crea una función de nombre sonMayores:
- Debe recibir mediante parámetros con nombre un número y una lista de números decimales.
- La lista debe ser obligatoria.
- Si no se envía el número, debe utilizarse el valor
0. - Emplea el método
everypara comprobar si todos los elementos de la lista son mayores que el número indicado. - La función debe devolver la cadena
'SI'o'NO'. - Llama a la función empleando los valores
2y12. - Muestra una frase con el formato:
Todos los números de la lista son mayores a XX: YY
- siendo
XXel número utilizado eYYel resultadoSIoNO.
Ejercicio 5:
Emplea List.generate para realizar los siguientes ejercicios:
- Genera una lista de 100 números enteros aleatorios comprendidos entre 1 y 100.
- Genera una lista con los números comprendidos entre 1 y 100 y haz que tenga longitud fija, de forma que no puedan añadirse ni eliminarse elementos.
- Dado un número, genera una lista con los cuadrados de todos los números comprendidos entre 1 y dicho número. En este caso, la lista debe permitir añadir nuevos elementos.
Ejercicio adicional:
Investiga otros métodos disponibles en List, Set, Map o Iterable que reciban una función como parámetro.
- Busca varios ejemplos.
- Indica qué tipo de función espera cada método.
- Realiza un pequeño ejemplo de uso.
- Algunos métodos que puedes investigar son
any,firstWhere,fold,removeWhereoupdateAll.
Soluciones de los ejercicios propuestos
Solución Ejercicio 1:
String devolverElementoLista({ int posicion = 0, required List<String> lista, }) { if (lista.isEmpty) { return 'Sin datos'; } if ( posicion < 0 || posicion >= lista.length ) { return 'Índice fuera de rango'; } return lista[posicion]; } void main() { final lista = <String>[ 'Uno', 'Dos', 'Tres', 'Cuatro', ]; print( devolverElementoLista( lista: lista, ), ); print( devolverElementoLista( posicion: 2, lista: lista, ), ); print( devolverElementoLista( lista: lista, posicion: 4, ), ); print( devolverElementoLista( posicion: -1, lista: lista, ), ); }
Solución Ejercicio 2:
int getNumPlatosMasCaros({ required Map<int, double> mapa, required double precio, }) { return mapa.values .where( (precioMapa) => precioMapa > precio, ) .length; } void main() { final precios = <int, double>{ 1: 10.34, 2: 3.24, 3: 24.99, 4: 17.35, }; precios.addAll({ 5: 9.23, 6: 2.22, }); print(precios); final ultimaClave = precios.keys.last; precios.remove(ultimaClave); precios.forEach((clave, valor) { if (valor < 5) { print( 'La clave $clave tiene un precio de $valor', ); } }); const precioReferencia = 10.0; final numPlatos = getNumPlatosMasCaros( mapa: precios, precio: precioReferencia, ); print( 'Número de platos con un precio superior ' 'a $precioReferencia euros: $numPlatos', ); }
Solución Ejercicio 3:
void buscarCadena( Set<String> conjunto, String cadena, ) { final encontrados = conjunto.where( (elemento) => elemento.contains(cadena), ); for (final elemento in encontrados) { print('Encontrado: $elemento'); } } void main() { final equipo = <String>{ 'Base', 'Alero', 'Pivot', 'Escolta', }; var cadenaBusqueda = 'as'; print( 'Cadena de búsqueda: $cadenaBusqueda', ); buscarCadena( equipo, cadenaBusqueda, ); cadenaBusqueda = 's'; print( 'Cadena de búsqueda: $cadenaBusqueda', ); buscarCadena( equipo, cadenaBusqueda, ); }
Solución Ejercicio 4:
String sonMayores({ double numero = 0, required List<double> lista, }) => lista.every( (elemento) => elemento > numero, ) ? 'SI' : 'NO'; void main() { final listaNumeros = <double>[ 10.0, 11.22, 15.0, 7.0, 3.44, ]; var numeroReferencia = 2.0; print( 'Todos los números de la lista son mayores ' 'a $numeroReferencia: ' '${sonMayores( numero: numeroReferencia, lista: listaNumeros, )}', ); numeroReferencia = 12.0; print( 'Todos los números de la lista son mayores ' 'a $numeroReferencia: ' '${sonMayores( lista: listaNumeros, numero: numeroReferencia, )}', ); }
Solución Ejercicio 5:
import 'dart:math'; void main() { final random = Random(); // 100 números aleatorios entre 1 y 100. final numerosAleatorios = List<int>.generate( 100, (_) => random.nextInt(100) + 1, ); print(numerosAleatorios); // Números desde 1 hasta 100. // Lista de longitud fija. final numerosSecuenciales = List<int>.generate( 100, (posicion) => posicion + 1, growable: false, ); print(numerosSecuenciales); // Cuadrados desde 1 hasta numero. // La lista permite añadir elementos. const numero = 10; final numerosAlCuadrado = List<int>.generate( numero, (posicion) { final valor = posicion + 1; ``` return valor * valor; }, growable: true, ``` ); print(numerosAlCuadrado); numerosAlCuadrado.add(121); print(numerosAlCuadrado); }
Enlace a la página principal de la UD2
Enlace a la página principal del curso