A library for managing state and communicating across dynamic forms and widget trees.

Features

Widgets can pass events to other interstate widgets in the tree.

Widgets can query state from widgets below them in the tree.

Dynamically added widgets are automatically connected to the interstate.

Build dynamic forms without mirroring the structure in your data model.

Widgets in your form can manage their own states and children, without a higher level state object to define structures.

Sample

Basic explainer

This is not intended as a replacement for a more powerful state management solution, this is generally useful when constructing forms that have interesting dynamic properties and or convoluted validation scenarios.

When a new InterstateWidget attaches to the tree, it registers itself with the nearest ancestor InterstateWidget. This is how we pass events through the tree.

EventHandlers define which event types any particular InterstateWidget cares about.

At the time of firing an event, we can decide how we want to broadcast the event.

The event is always sent to the local handler.

  • if the down flag is set to true: The event will be sent to every interstate widget below us in the tree.
  • if the up flag is set to true: The event will be sent to up the tree until the root InterstateWidget is reached.
  • if the span flag is set to true (with the up flag also set to true): The event will travel up the tree, and then back down any branch it encounters.

Getting started

1. Depend on it

Add this to your package's pubspec.yaml file:

dependencies:
  interstate: '^1.0.0'

2. Install it

You can install packages from the command line:

$ pub get
..

Alternatively, your editor might support pub. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:

import 'package:interstate/interstate.dart';

Usage

1. Define a state object

class NestedTextFieldState extends Equatable {
  final String text;
  final String error;

  const NestedTextFieldState({required this.text, this.error = ""});

  NestedTextFieldState copyWith({String? text, String? error}) =>
      NestedTextFieldState(text: text ?? this.text, error: error ?? this.error);

  @override
  List<Object?> get props => [text, error];
}

2. Define an interstate widget

InterstateWidget<NestedTextFieldState>(
      id: uniqueId,
      widgetBuilder: (BuildContext context, NestedTextFieldState state, Widget? child, InterstateController<NestedTextFieldState> controller) {
            return TextFormField(
              initialValue: state.text,
              decoration: InputDecoration(
                errorText: state.error,
                border: const OutlineInputBorder()
              ),
              onChanged: (text) {
                // We send a String as an event to the local controller
                controller.send(text);
                // Then we send a validation event up and down the widget tree
                controller.send(EventValidate(), up: true, down: true);
              },
              onEditingComplete: () {
                // We can query the validation state including the validation of our children at save time!
                if ((controller.send(EventValidate(), down: true).result as ValidationResponse).canSave == true) {
                  // We have valid data!
                  debugPrintSynchronously("VALID DATA:${state.text}");
                }
              },
            ),
        );
      },
      controllerInitializer: (String id, bubbleUpListener) {
        return InterstateController<NestedTextFieldState>(
          id: id,
          initialState: const NestedTextFieldState(text: ""),
          bubbleUpListener: bubbleUpListener,
          handlers: [],
        );
      },
      child: child,
    );

3. Define handlers to respond to events

...
handlers: [
    /// This is a useful example of using the supplied EventValidate and ValidationResponse classes
    /// We can validate our state, and differentiate local errors and child errors
    EventHandler<EventValidate, NestedTextFieldState>(
        eventDataType: EventValidate,
        resultBuilder: (Event<EventValidate> event, NestedTextFieldState currentState,
                List<Response<EventValidate, dynamic>> subResponses) =>
            ValidationResponse.fromSubResponses(
                // The validation error relates to the
                validationError: currentState.text.isEmpty ? "Required!" : null,
                // Here we could pass along an existing error message if we wanted, but we just want to replace
                // any existing error messages, not propagate them
                stateError: null,
                // We pass along a set of all the child validation responses, and if we wanted we could do
                // something with them
                subResponses: subResponses),
        stateBuilder: (event, state, result, subresponses) {
          // The result comes from the resultBuilder, so we know it is a ValidationResponse
          final validationResp = result as ValidationResponse;
    
          if (validationResp.dataValidationError != null) {
            // If we have a local validation error, we update our state accordingly
            return state.copyWith(error: validationResp.dataValidationError ?? "");
          } else if (validationResp.childrenHaveErrors) {
            // If our children have errors, we also report an error
            return state.copyWith(error: "Children have errors!");
          } else {
            // Otherwise clear the error
            return state.copyWith(error: "");
          }
        }),
    /// This takes a String event and uses it to update the state text. Generally we would like to wrap something
    /// like this in its own StringUpdateEvent class, but for example code we can call this a demonstration of
    /// typing flexibility
    EventHandler<String, NestedTextFieldState>(
      eventDataType: String,
      resultBuilder: (_, __, ___) => null,
      stateBuilder: (event, currentState, ___, ____) => currentState.copyWith(text: event.data, error: ""),
    ),
],
...

Additional information

https://github.com/jdrotos/interstate