Flutter DART Funciones

De MediaWiki
Revisión del 07:34 24 sep 2026 de Angelfg (discusión | contribuciones) (→‎Ejercicios propuestos)
(dif) ← Revisión anterior | Revisión actual (dif) | Revisión siguiente → (dif)
Ir a la navegación Ir a la búsqueda

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? o double?.


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.
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:
  • valorPrueba sigue valiendo 5.
  • cadena sigue conteniendo 'VIEJA CADENA'.
  • Las propiedades del objeto pelota sí 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, mensaje tendrá el valor null.
  • 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 sea null.


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 forEach de un Iterable espera 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 elemento representa, 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 utilizar for...in en lugar de forEach con 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 recorrerConjunto en ese momento. Queremos proporcionar la función a forEach para 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 un String y devuelve void:
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);
recorrerConjunto representa la función que queremos proporcionar a forEach.
  • 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, forEach es 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 Map también disponen del método forEach.
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 objetos Iterable, por lo que para un recorrido sencillo podemos utilizar for...in:
void main() {
final conjunto = <String>{
'Ángel',
'Luis',
'Ana',
'María',
};

for (final elemento in conjunto) {
print(elemento);
}
}
  • También podemos utilizar forEach si 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 where permite filtrar los elementos de un Iterable.
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);
}
where devuelve un Iterable.
  • 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 every permite comprobar si todos los elementos de un Iterable cumplen 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 map permite transformar los elementos de un Iterable.
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 map sobre las entradas de un Map:
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 List dispone del constructor List.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: false que la lista tenga longitud fija:
final numeros = List<int>.generate(
5,
(posicion) => posicion + 1,
growable: false,
);

Importante:

Una lista con growable: false es 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 getNumPlatosMasCaros a 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 where para 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'.


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 every para 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 2 y 12.
  • Muestra una frase con el formato:
Todos los números de la lista son mayores a XX: YY
siendo XX el número utilizado e YY el resultado SI o NO.


Ejercicio 5:

Emplea List.generate para realizar los siguientes ejercicios:

  • Genera una lista de 100 números enteros aleatorios comprendidos entre 1 y 100. Emplea la clase Random.
  • 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, removeWhere o updateAll.



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


```