Skip to content

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

dart
// 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

dart

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.

    dart
    intWatcher.updateIf((val) => val < 10, 5);
    intWatcher.updateIf((val) => val < 10, 5);
  • onChange: Perform action on value change.

    dart
    stringWatcher.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.

    dart
    final 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.

    dart
    numWatcher.debounce(Duration(seconds: 1), (val) => print(val));
    numWatcher.debounce(Duration(seconds: 1), (val) => print(val));
  • map: Transform the value to another type.

    dart
    final stringWatcher = intWatcher.map((val) => val.toString());
    final stringWatcher = intWatcher.map((val) => val.toString());
  • combineWith: Combine with another Watcher.

    dart
    final 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>.

    dart
    boolWatcher.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.

    dart
    listWatcher.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.

    dart
    numWatcher.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.

    dart
    mapWatcher.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.

    dart
    setWatcher.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>.

    dart
    uriWatcher.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.

dart
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.

dart
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.

dart
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

dart
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),
        ),
      ),
    );
  }
}