Keeping State topic

This topic shows you multiple ways to keep state in your use cases.

Table of Contents:

Ways to Keep State

When writing a use case, you may encounter widgets that require you to keep changing state outside the widget. This state usually comes in one of two forms:

  • Immutable state: Often passed as a pair of a value and onValueChanged callback into the widget.
  • Mutable state: Often a controller or another object that has its own lifecycle and needs creation and disposal.

You could wrap your returned use case widget in a StatefulWidget and manage the state within it. However, this approach requires significant boilerplate code. Do not do this!

Instead, Werkbank provides better alternatives to keep state in your use case:

  • Knobs keep state in a way that allows you to interactively change values in the Werkbank UI.
  • The StateKeepingAddon allows you to store any immutable or mutable state in your use case.

Knobs

Learn all about knobs in the Knobs topic or read a summary in the Knobs section of the Writing Use Cases topic.

Prefer using knobs to keep state when:

  • You benefit from interactive controls in your Werkbank UI for testing different values.
  • There is an existing knob for your data type.

StateKeepingAddon

Use the StateKeepingAddon to keep state when:

This example keeps a Color and a TextEditingController for the use case:

WidgetBuilder myColorPickerUseCase(UseCaseComposer c) {
  // Keep immutable state in a ValueNotifier
  final colorNotifier = c.states.immutable(
    'Color',
    initialValue: Colors.red,
  );

  // Keep mutable state and provide functions to create and dispose it.
  final hexControllerContainer = c.states.mutable(
    'Hex Controller',
    create: TextEditingController.new,
    dispose: (controller) => controller.dispose(),
  );

  return (context) {
    return MyColorPicker(
      // Get and set the color using the ValueNotifier
      color: colorNotifier.value,
      onColorChanged: (newColor) => colorNotifier.value = newColor,
      // Unpack the returned ValueContainer to get the TextEditingController
      hexColorController: hexControllerContainer.value,
    );
  };
}
Equivalent example using only a StatefulWidget.

This illustrates the issue that the StateKeepingAddon solves for you, since you don't have to do this:

WidgetBuilder myColorPickerUseCase(UseCaseComposer c) {
  return (context) {
    return _MyColorPickerStateProvider(
      builder: (context, color, setColor, hexColorController) {
        return MyColorPicker(
          color: color,
          onColorChanged: setColor,
          hexColorController: hexColorController,
        );
      },
    );
  };
}

class _MyColorPickerStateProvider extends StatefulWidget {
  const _MyColorPickerStateProvider({
    required this.builder,
  });

  final Widget Function(
    BuildContext context,
    Color color,
    ValueChanged<Color> setColor,
    TextEditingController hexColorController,
    )
  builder;

  @override
  State<_MyColorPickerStateProvider> createState() =>
    _MyColorPickerStateProviderState();
}

class _MyColorPickerStateProviderState
  extends State<_MyColorPickerStateProvider> {
  Color _color = Colors.red;
  final TextEditingController _hexColorController = TextEditingController();

  @override
  void dispose() {
    _hexColorController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(
      context,
      _color,
      (newColor) => setState(() => _color = newColor),
      _hexColorController,
    );
  }
}

Keeping Immutable State

Use c.states.immutable(...) to store immutable state. This method returns a ValueNotifier that you can read from and write to.

WidgetBuilder myCounterUseCase(UseCaseComposer c) {
  final counterNotifier = c.states.immutable(
    'Counter',
    initialValue: 0,
  );

  return (context) {
    return MyCounter(
      counter: counterNotifier.value,
      onIncrement: () => counterNotifier.value++,
      onDecrement: () => counterNotifier.value--,
    );
  };
}

You can read and write the value of the ValueNotifier inside the returned WidgetBuilder. When the value of an immutable state changes, the use case will rebuild.

Keeping Mutable State

Use c.states.mutable(...) to store mutable state. This method returns a ValueContainer that holds your mutable object.

The mutable method requires:

  • A create callback that creates the object.
  • A dispose callback that properly disposes of the object.
WidgetBuilder myTextFieldUseCase(UseCaseComposer c) {
  final scrollControllerContainer = c.states.mutable(
    'Scroll Controller',
    create: () => ScrollController(),
    dispose: (controller) => controller.dispose(),
  );

  return (context) {
    return MyScrollView(
      scrollController: scrollControllerContainer.value,
      onScrollToTopPressed: () => scrollControllerContainer.value.animateTo(0),
    );
  };
}

You can read the value of the ValueContainer inside the returned WidgetBuilder. Unlike with immutable states, you cannot reassign the value of a ValueContainer later. You can only mutate the contained value. Mutating the contained value will not trigger a rebuild of the use case.

Use c.states.mutableWithTickerProvider(...) for objects that require a TickerProvider.

An extension also offers some convenience methods for common mutable objects:

Extensions

CommonStatesComposerExtension on StatesComposer Keeping State
This extension provides some convenience methods for common state types.