finite_state_widget 0.0.1 copy "finite_state_widget: ^0.0.1" to clipboard
finite_state_widget: ^0.0.1 copied to clipboard

A small Flutter package that implements a Finite State Machine (FSM) pattern for widgets.

pub package

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 FiniteStateController to encapsulate business logic and trigger transitions from outside the widget.
  • Safe async flows: controllers and async methods should check mounted before 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 initialState
    • Transitions<S, E> get transitions (map (S, E) -> S)
    • Map<S, Widget> get buildByState
    • Optional: override createController() to provide a FiniteStateController.
  • FiniteStateController<S, E>: controller with helpers:
    • dispatch(E event) to dispatch events
    • goTo(S state) to jump states
    • currentState, context, and mounted accessors

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 mounted in async controller methods before dispatching.
  • Give each state widget a unique ValueKey to avoid AnimatedSwitcher glitches.

🧾 License #

MIT Β© ccisne.dev https://ccisne.dev

0
likes
150
points
13
downloads

Publisher

verified publisherccisne.dev

Weekly Downloads

A small Flutter package that implements a Finite State Machine (FSM) pattern for widgets.

Repository (GitHub)
View/report issues

Topics

#widget #state-management #macss

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on finite_state_widget