Watcher (State Management)
Overview
Watcher is a simple state management that utilizes Flutter's native ValueListenableBuilder
and ValueNotifier
, providing a lightweight, efficient, and straightforward way to manage state changes.
Initialization
// you can use the `.watch` Extension.
final boolWatcher = false.watch; // Initializes a BoolWatcher
final intWatcher = 42.watch; // Initializes a IntWatcher
final listWatcher = [1, 2, 3].watch; // Initializes a ListWatcher<int>
// and so on for other types like List, Set, Map, num, Uri
// Or use the original Watchers.
final boolWatcher = BoolWatcher(true);
final listWatcher = ListWatcher<int>([1, 2, 3]);
final intWatcher = IntWatcher(10);
// and similarly for DoubleWatcher, StringWatcher, ColorWatcher, etc.
// You can also use the oringinal syntax.
final boolWatcher = Watcher<bool>(true);
// Flutter's ValueNotifier also supported
final boolWatcher = ValueNotifier<bool>(true);
// you can use the `.watch` Extension.
final boolWatcher = false.watch; // Initializes a BoolWatcher
final intWatcher = 42.watch; // Initializes a IntWatcher
final listWatcher = [1, 2, 3].watch; // Initializes a ListWatcher<int>
// and so on for other types like List, Set, Map, num, Uri
// Or use the original Watchers.
final boolWatcher = BoolWatcher(true);
final listWatcher = ListWatcher<int>([1, 2, 3]);
final intWatcher = IntWatcher(10);
// and similarly for DoubleWatcher, StringWatcher, ColorWatcher, etc.
// You can also use the oringinal syntax.
final boolWatcher = Watcher<bool>(true);
// Flutter's ValueNotifier also supported
final boolWatcher = ValueNotifier<bool>(true);
Using ValueWatcher
Widget
final isLoading = false.watch;
@override
Widget build(BuildContext context) {
...
ValueWatcher<MyType>(
builder: (context, value) => MyWidget(value), // Builder function
watcher: isLoading, // Your watcher instance.
),
...
}
// or with the `.watcher` Extension
@override
Widget build(BuildContext context) {
...
isLoading.watcher(
builder: (context, value) => MyWidget(value),
),
...
}
// my `MyWidget` here will automatically update its value, when isLoading.value changes.
final isLoading = false.watch;
@override
Widget build(BuildContext context) {
...
ValueWatcher<MyType>(
builder: (context, value) => MyWidget(value), // Builder function
watcher: isLoading, // Your watcher instance.
),
...
}
// or with the `.watcher` Extension
@override
Widget build(BuildContext context) {
...
isLoading.watcher(
builder: (context, value) => MyWidget(value),
),
...
}
// my `MyWidget` here will automatically update its value, when isLoading.value changes.
Handling Changes and Utilities
Description
Extensions such as updateIf
, onChange
, debounce
, map
, and combineWith
provide powerful utilities for responding to changes, debouncing actions, transforming, or combining multiple Watcher
instances.
Usage Examples
updateIf: Update the value conditionally.
dartintWatcher.updateIf((val) => val < 10, 5);
intWatcher.updateIf((val) => val < 10, 5);
onChange: Perform action on value change.
dartstringWatcher.onChange((val) => print(val)); // it returns call back that disposes the listener do not forget to call it in your close/dispose methods.
stringWatcher.onChange((val) => print(val)); // it returns call back that disposes the listener do not forget to call it in your close/dispose methods.
stream: Convert
Watcher
changes into a stream.dartfinal streamWatcher = valueWatcher.stream; streamWatcher.listen((value) { // Handle the stream of changes });
final streamWatcher = valueWatcher.stream; streamWatcher.listen((value) { // Handle the stream of changes });
debounce: Debounce value changes.
dartnumWatcher.debounce(Duration(seconds: 1), (val) => print(val));
numWatcher.debounce(Duration(seconds: 1), (val) => print(val));
map: Transform the value to another type.
dartfinal stringWatcher = intWatcher.map((val) => val.toString());
final stringWatcher = intWatcher.map((val) => val.toString());
combineWith: Combine with another
Watcher
.dartfinal combinedWatcher = intWatcher.combineWith(stringWatcher, (int a, String b) => '$a and $b');
final combinedWatcher = intWatcher.combineWith(stringWatcher, (int a, String b) => '$a and $b');
Other Useful Helpers
Allowing direct manipulation of values in a more natural and concise way, mirroring the operations you'd typically perform on the data types themselves. Here are some samples:
Bool
toggle: Toggle the value of
Watcher<bool>
.dartboolWatcher.toggle(); // Toggles the boolean value
boolWatcher.toggle(); // Toggles the boolean value
List
addAll: Add multiple items to
Watcher<List<E>>
.remove: Remove an item from the list.
clear: Clear all items in the list.
dartlistWatcher.addAll([4, 5]); // Adds items to the list listWatcher.remove(item); // Removes an item listWatcher.clear(); // Clears the list
listWatcher.addAll([4, 5]); // Adds items to the list listWatcher.remove(item); // Removes an item listWatcher.clear(); // Clears the list
Num/int/double
increment: Increment the value of
Watcher<num>
.decrement: Decrement the value.
dartnumWatcher.increment(); // Increments the number numWatcher.decrement(); // Decrements the number
numWatcher.increment(); // Increments the number numWatcher.decrement(); // Decrements the number
Map
putIfAbsent: Add a key-value pair if the key is not already in the map.
remove: Remove a key-value pair.
dartmapWatcher.putIfAbsent(key, () => value); // Adds key-value pair if absent mapWatcher.remove(key); // Removes the key-value pair
mapWatcher.putIfAbsent(key, () => value); // Adds key-value pair if absent mapWatcher.remove(key); // Removes the key-value pair
Set
add: Add an item to
Watcher<Set<T>>
.removeAll: Remove all items from the set.
dartsetWatcher.add(item); // Adds an item setWatcher.removeAll(items); // Removes all specified items
setWatcher.add(item); // Adds an item setWatcher.removeAll(items); // Removes all specified items
Uri
updatePath: Update the path of
Watcher<Uri>
.darturiWatcher.updatePath(newPath); // Updates the URI path
uriWatcher.updatePath(newPath); // Updates the URI path
Flutter Controllers With Watcher
The Watcher provides seamless integration with various native Flutter controllers, enhancing the development of reactive UIs. Below are examples demonstrating how Watcher can be utilized with common Flutter controllers:
TextEditingController
Watcher can reactively handle text input changes, making it ideal for scenarios like live character counts or conditional UI updates based on user input.
final textEditingController = TextEditingController();
ValueWatcher(
watcher: textEditingController,
builder: (context, TextEditingValue value) {
return Column(
children: [
TextField(controller: textEditingController),
Text('Character Count: ${value.text.length}'),
],
);
},
);
final textEditingController = TextEditingController();
ValueWatcher(
watcher: textEditingController,
builder: (context, TextEditingValue value) {
return Column(
children: [
TextField(controller: textEditingController),
Text('Character Count: ${value.text.length}'),
],
);
},
);
AnimationController
Incorporate Watcher with AnimationController for dynamic UI elements that respond to animation state changes, enhancing the visual experience.
final animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat(reverse: true);
ValueWatcher(
watcher: animationController,
builder: (context, double value) {
double scale = 1 + value; // Adjust scale based on animation value
return Transform.scale(
scale: scale,
child: MyAnimatedWidget(),
);
},
);
final animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat(reverse: true);
ValueWatcher(
watcher: animationController,
builder: (context, double value) {
double scale = 1 + value; // Adjust scale based on animation value
return Transform.scale(
scale: scale,
child: MyAnimatedWidget(),
);
},
);
ScrollController
Utilize Watcher with ScrollController to create responsive UI components that react to user's scroll behavior, such as showing or hiding elements based on scroll position.
final scrollController = ScrollController();
Stack(
children: [
MyScrollableContent(controller: scrollController),
ValueWatcher(
watcher: scrollController,
builder: (context, ScrollPosition value) {
bool showTopButton =
value.pixels > 500; // Show button after scrolling down
return showTopButton
? Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: () => scrollController.animateTo(0,
duration: Duration(seconds: 1),
curve: Curves.easeOut),
child: Icon(Icons.arrow_upward),
),
)
: SizedBox.shrink();
},
),
],
);
final scrollController = ScrollController();
Stack(
children: [
MyScrollableContent(controller: scrollController),
ValueWatcher(
watcher: scrollController,
builder: (context, ScrollPosition value) {
bool showTopButton =
value.pixels > 500; // Show button after scrolling down
return showTopButton
? Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: () => scrollController.animateTo(0,
duration: Duration(seconds: 1),
curve: Curves.easeOut),
child: Icon(Icons.arrow_upward),
),
)
: SizedBox.shrink();
},
),
],
);
Full Counter Example
import 'package:flutter/material.dart';
import 'package:flutter_helper_utils/flutter_helper_utils.dart';
main() => runApp(MyCounter());
class MyCounter extends StatelessWidget {
MyCounter({super.key});
// this can also be initialized in another class (e.g. singleton class)
// and change its value any where for more scaled state management
final IntWatcher counter = 0.watch;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Watch Counter',
home: Scaffold(
appBar: AppBar(title: const Text('Watch Counter')),
body: Center(
child: ValueWatcher(
watcher: counter,
builder: (context, value) {
return Text('Counter: $value');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.increment(1),
child: const Icon(Icons.add),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_helper_utils/flutter_helper_utils.dart';
main() => runApp(MyCounter());
class MyCounter extends StatelessWidget {
MyCounter({super.key});
// this can also be initialized in another class (e.g. singleton class)
// and change its value any where for more scaled state management
final IntWatcher counter = 0.watch;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Watch Counter',
home: Scaffold(
appBar: AppBar(title: const Text('Watch Counter')),
body: Center(
child: ValueWatcher(
watcher: counter,
builder: (context, value) {
return Text('Counter: $value');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.increment(1),
child: const Icon(Icons.add),
),
),
);
}
}