finite_state_widget 0.0.1
finite_state_widget: ^0.0.1 copied to clipboard
A small Flutter package that implements a Finite State Machine (FSM) pattern for widgets.
finite_state_widget #
A small Flutter package that implements a Finite State Machine (FSM) pattern for widgets.
One-liner: Declarative FSM-driven widgets with optional controllers (MVC/MVVM style) and an easy map-based UI per-state.
β¨ Features #
- Declare state machines using enums and a transition map:
(state, event) -> nextState. - Map states to widgets via
buildByStateβ the package handles switching and animation (AnimatedSwitcher). - Optional, attachable
FiniteStateControllerto encapsulate business logic and trigger transitions from outside the widget. - Safe async flows: controllers and async methods should check
mountedbefore dispatching. - Lightweight lifecycle hooks:
onEnter,onExit,onInvalidTransition,onSelfTransition. - Small, test-friendly controllers: you can unit-test controller logic without building widgets.
π¦ Installation #
In your pubspec.yaml:
dependencies:
finite_state_widget: ^0.0.1
Or add via command line:
dart pub add finite_state_widget
Then run:
dart pub get
π Quick start #
Define your states and events as enums, create a FiniteStateWidget, implement a FiniteState that provides initialState, transitions and buildByState.
import 'package:flutter/material.dart';
import 'package:finite_state_widget/finite_state_widget.dart';
enum LoadState { idle, loading, ready, empty, error }
enum LoadEvent { start, succeed, emptyResult, fail, reset }
class LoaderController extends FiniteStateController<LoadState, LoadEvent> {
Future<void> load() async {
dispatch(LoadEvent.start);
await Future.delayed(const Duration(seconds: 1));
if (mounted) dispatch(LoadEvent.succeed);
}
void reset() => dispatch(LoadEvent.reset);
}
class LoaderWidget extends FiniteStateWidget {
const LoaderWidget({super.key, this.controller});
final LoaderController? controller;
@override
FiniteState createFiniteState() => _LoaderState();
}
class _LoaderState extends FiniteState<LoaderWidget, LoadState, LoadEvent> {
@override
FiniteStateController<LoadState, LoadEvent>? createController() =>
widget.controller ?? LoaderController();
LoaderController? get _ctrl => controller as LoaderController?;
@override
LoadState get initialState => LoadState.idle;
@override
Transitions<LoadState, LoadEvent> get transitions => {
(LoadState.idle, LoadEvent.start): LoadState.loading,
(LoadState.loading, LoadEvent.succeed): LoadState.ready,
(LoadState.loading, LoadEvent.emptyResult): LoadState.empty,
(LoadState.loading, LoadEvent.fail): LoadState.error,
(LoadState.ready, LoadEvent.reset): LoadState.idle,
(LoadState.empty, LoadEvent.reset): LoadState.idle,
(LoadState.error, LoadEvent.reset): LoadState.idle,
};
@override
Map<LoadState, Widget> get buildByState => {
LoadState.idle: ElevatedButton(
onPressed: _ctrl?.load,
child: const Text('Load'),
),
LoadState.loading: const Center(child: CircularProgressIndicator()),
LoadState.ready: const Center(child: Text('Loaded')),
LoadState.empty: const Center(child: Text('No data')),
LoadState.error: ElevatedButton(
onPressed: () => dispatch(LoadEvent.reset),
child: const Text('Retry'),
),
};
}
Mount LoaderWidget in your app. You may pass a controller from a parent to control the widget externally.
π§ API overview #
- FiniteStateWidget: base widget class; override
createFiniteState(). - FiniteState<T, S, E>: state class. Required:
S get initialStateTransitions<S, E> get transitions(map(S, E) -> S)Map<S, Widget> get buildByState- Optional: override
createController()to provide aFiniteStateController.
- FiniteStateController<S, E>: controller with helpers:
dispatch(E event)to dispatch eventsgoTo(S state)to jump statescurrentState,context, andmountedaccessors
Lifecycle hooks on FiniteState:
onEnter(newState, prevState)onExit(oldState, nextState)onInvalidTransition(current, event)onSelfTransition(current, event)
π§ͺ Testing controllers without widgets #
Controllers are simple Dart classes that call dispatch(...). To unit-test controller logic without building widgets, create a small test subclass overriding dispatch and mounted.
Example (test):
class TestController extends LoaderController {
final events = <String>[];
@override
bool dispatch(event) {
events.add(event.toString());
return true;
}
@override
bool get mounted => true;
}
This allows calling async controller methods and asserting they call dispatch with the expected events.
π Example #
See the example/ folder in this package for a working demo that shows:
- External controller usage (parent passes controller to widget).
- Internal controller creation (widget creates its own controller).
- A small demo app wiring multiple FSM widgets.
β Best practices #
- Keep FSM states and events as enums for type safety.
- Prefer controllers for business logic and async flows; controllers should not manipulate UI directly.
- Check
mountedin async controller methods before dispatching. - Give each state widget a unique
ValueKeyto avoid AnimatedSwitcher glitches.
π§Ύ License #
MIT Β© ccisne.dev https://ccisne.dev