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
andonValueChanged
callback into the widget. - Mutable state: Often a controller or another object that has its own lifecycle and needs creation and disposal.
- Example types include: ScrollController, TextEditingController, FocusNode, and other objects that manage their own state.
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.
- If you need a knob but no suitable one exists, consider implementing a custom knob.
StateKeepingAddon
Use the StateKeepingAddon to keep state when:
- Working with Flutter controllers (TextEditingController, ScrollController, TabController) or other mutable objects like controllers.
- Managing immutable data models.
- You don't want interactive controls in your Werkbank UI.
- Your data type in not easily represented using a knob and a writing a custom knob would be overkill.
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:
Classes
Extensions
- CommonStatesComposerExtension on StatesComposer Keeping State
- This extension provides some convenience methods for common state types.