Operators de Combinación
Los operators de combinación te permiten fusionar múltiples ValueListenables en un único observable, actualizándose cuando cualquier fuente cambia.
combineLatest()
Combina dos ValueListenables usando una función combinadora. El resultado se actualiza cuando cualquiera de las fuentes cambia.
Uso Básico
void main() {
final isLoadingData = ValueNotifier<bool>(false);
final isLoadingUser = ValueNotifier<bool>(false);
// Combine two loading states - true if either is loading
final isLoading = isLoadingData.combineLatest<bool, bool>(
isLoadingUser,
(dataLoading, userLoading) => dataLoading || userLoading,
);
isLoading.listen((loading, _) => print('Loading: $loading'));
// Prints initial: Loading: false
isLoadingData.value = true;
// Prints: Loading: true
isLoadingUser.value = true;
// Prints: Loading: true (both loading)
isLoadingData.value = false;
// Prints: Loading: true (user still loading)
isLoadingUser.value = false;
// Prints: Loading: false (both done)
}Cómo Funciona
combineLatest() crea un nuevo ValueListenable que:
- Mantiene el último valor de ambas fuentes
- Llama a la función combinadora cuando cualquiera de las fuentes cambia
- Notifica a los listeners con el resultado combinado
Parámetros de Tipo
combineLatest<TIn2, TOut>() recibe dos parámetros de tipo:
TIn2- Tipo del segundo ValueListenableTOut- Tipo del resultado combinado
final ageNotifier = ValueNotifier<int>(25);
final nameNotifier = ValueNotifier<String>('John');
// Combinar int y String en un tipo personalizado
final user = ageNotifier.combineLatest<String, User>(
nameNotifier,
(int age, String name) => User(age: age, name: name),
);Casos de Uso Comunes
Validación de Formularios
final email = ValueNotifier<String>('');
final password = ValueNotifier<String>('');
final isValid = email.combineLatest<String, bool>(
password,
(e, p) => e.contains('@') && p.length >= 8,
);
ValueListenableBuilder<bool>(
valueListenable: isValid,
builder: (context, valid, _) => ElevatedButton(
onPressed: valid ? _submit : null,
child: Text('Submit'),
),
);Valores Computados
final quantity = ValueNotifier<int>(1);
final price = ValueNotifier<double>(9.99);
final total = quantity.combineLatest<double, double>(
price,
(qty, p) => qty * p,
);
print(total.value); // 9.99
quantity.value = 3;
print(total.value); // 29.97UI Condicional
final isDarkMode = ValueNotifier<bool>(false);
final fontSize = ValueNotifier<double>(14.0);
final textStyle = isDarkMode.combineLatest<double, TextStyle>(
fontSize,
(dark, size) => TextStyle(
color: dark ? Colors.white : Colors.black,
fontSize: size,
),
);Estado de Múltiples Fuentes
final isLoading = ValueNotifier<bool>(false);
final hasError = ValueNotifier<bool>(false);
final uiState = isLoading.combineLatest<bool, UIState>(
hasError,
(loading, error) {
if (loading) return UIState.loading;
if (error) return UIState.error;
return UIState.ready;
},
);Combinando Más de Dos Fuentes
Para combinar 3-6 ValueListenables, usa combineLatest3 hasta combineLatest6:
final source1 = ValueNotifier<int>(1);
final source2 = ValueNotifier<int>(2);
final source3 = ValueNotifier<int>(3);
final sum = source1.combineLatest3<int, int, int>(
source2,
source3,
(a, b, c) => a + b + c,
);
print(sum.value); // 6Similarmente disponibles: combineLatest4, combineLatest5, combineLatest6
Cuándo Usar combineLatest()
Usa combineLatest() cuando:
- ✅ Necesites valores de 2-6 ValueListenables
- ✅ Quieras actualizar cuando cualquier fuente cambie
- ✅ Necesites combinar valores en un nuevo tipo
- ✅ Estés implementando estado derivado o propiedades computadas
mergeWith()
Fusiona cambios de valor de múltiples ValueListenables del mismo tipo. Se actualiza cuando cualquier fuente cambia, emitiendo el valor de esa fuente.
Uso Básico
void main() {
final listenable1 = ValueNotifier<int>(0);
final listenable2 = ValueNotifier<int>(0);
final listenable3 = ValueNotifier<int>(0);
// Merge multiple ValueListenables - updates when ANY changes
listenable1.mergeWith([listenable2, listenable3]).listen((x, _) => print(x));
listenable2.value = 42;
// Prints: 42
listenable1.value = 43;
// Prints: 43
listenable3.value = 44;
// Prints: 44
listenable2.value = 45;
// Prints: 45
listenable1.value = 46;
// Prints: 46
}Cómo Funciona
mergeWith() crea un nuevo ValueListenable que:
- Se suscribe a la fuente principal y todas las fuentes en la lista
- Cuando cualquier fuente cambia, emite el valor actual de esa fuente
- Todas las fuentes deben ser del mismo tipo
final source1 = ValueNotifier<int>(1);
final source2 = ValueNotifier<int>(2);
final source3 = ValueNotifier<int>(3);
final merged = source1.mergeWith([source2, source3]);
print(merged.value); // 1 (valor inicial de source1)
source2.value = 20;
print(merged.value); // 20 (source2 cambió)
source3.value = 30;
print(merged.value); // 30 (source3 cambió)
source1.value = 10;
print(merged.value); // 10 (source1 cambió)Casos de Uso Comunes
Múltiples Fuentes de Eventos
final userInput = ValueNotifier<String>('');
final apiResult = ValueNotifier<String>('');
final cacheData = ValueNotifier<String>('');
// Reaccionar a actualizaciones de cualquier fuente
final dataStream = userInput.mergeWith([apiResult, cacheData]);
dataStream.listen((data, _) => updateUI(data));Múltiples Disparadores
final saveButton = ValueNotifier<DateTime?>(null);
final autoSave = ValueNotifier<DateTime?>(null);
final shortcutKey = ValueNotifier<DateTime?>(null);
// Guardar disparado por cualquier acción
final saveTrigger = saveButton.mergeWith([autoSave, shortcutKey]);
saveTrigger.listen((timestamp, _) {
if (timestamp != null) performSave();
});Agregando Fuentes Similares
final sensor1 = ValueNotifier<double>(0.0);
final sensor2 = ValueNotifier<double>(0.0);
final sensor3 = ValueNotifier<double>(0.0);
// Monitorear cualquier cambio de sensor
final anySensorChange = sensor1.mergeWith([sensor2, sensor3]);
anySensorChange.listen((value, _) => checkThreshold(value));combineLatest() vs mergeWith()
| Característica | combineLatest() | mergeWith() |
|---|---|---|
| Número de fuentes | 2-6 | 1 + N (array) |
| Tipos de fuentes | Pueden ser diferentes | Deben ser del mismo tipo |
| Tipo de salida | Personalizado (vía combinador) | Mismo tipo que la fuente |
| Usar para | Combinar valores diferentes | Fusionar eventos similares |
| Valor de salida | Resultado de la función combinadora | Valor de la fuente que cambió |
Ejemplo: Dos estados de carga
final isLoadingData = ValueNotifier<bool>(false);
final isLoadingUser = ValueNotifier<bool>(false);
// combineLatest - combina ambos valores con lógica (operación OR)
final isLoading = isLoadingData.combineLatest<bool, bool>(
isLoadingUser,
(dataLoading, userLoading) => dataLoading || userLoading,
);
isLoadingData.value = true;
print(isLoading.value); // true (datos cargando)
isLoadingUser.value = true;
print(isLoading.value); // true (ambos cargando)
isLoadingData.value = false;
print(isLoading.value); // true (usuario aún cargando)
// mergeWith - solo toma el que cambió
final anyLoading = isLoadingData.mergeWith([isLoadingUser]);
isLoadingData.value = true;
print(anyLoading.value); // true (de isLoadingData)
isLoadingUser.value = false;
print(anyLoading.value); // false (de isLoadingUser - ¡no es lo que quieres!)
isLoadingData.value = false;
print(anyLoading.value); // false (de isLoadingData)Diferencia clave: combineLatest() aplica lógica a ambos valores, mientras que mergeWith() solo emite la fuente que cambió - ¡haciéndolo incorrecto para este caso de uso!
Cuándo Usar mergeWith()
Usa mergeWith() cuando:
- ✅ Tengas múltiples fuentes del mismo tipo
- ✅ Quieras reaccionar a cambios de cualquier fuente
- ✅ No necesites combinar valores, solo monitorear cualquier cambio
- ✅ Estés agregando streams de eventos similares
Encadenando con Otros Operators
Los operators de combinación funcionan bien con otros operators:
final firstName = ValueNotifier<String>('');
final lastName = ValueNotifier<String>('');
firstName
.combineLatest<String, String>(
lastName,
(first, last) => '$first $last',
)
.where((name) => name.trim().isNotEmpty)
.map((name) => name.toUpperCase())
.listen((name, _) => print(name));Ejemplo del Mundo Real
Total de carrito de compras con impuestos:
final subtotal = ValueNotifier<double>(0.0);
final taxRate = ValueNotifier<double>(0.1);
final total = subtotal.combineLatest<double, double>(
taxRate,
(sub, rate) => sub * (1 + rate),
);
// Usar en UI
ValueListenableBuilder<double>(
valueListenable: total,
builder: (context, value, _) => Text('Total: \$${value.toStringAsFixed(2)}'),
);