flutter_mvu 1.0.6 
flutter_mvu: ^1.0.6 copied to clipboard
A minimal Elm‑inspired Model‑View‑Update state management library for Flutter.
flutter_mvu 🚀🎉 #
A minimal Elm-inspired Model-View-Update (MVU) state management library for Flutter. Predictable, testable, and boilerplate-free! 😎✨
📦 Installation 🔧 #
- Add to 
pubspec.yaml:dependencies: flutter: sdk: flutter flutter_mvu: ^1.0.3 - Fetch packages:
flutter pub get - Import into your Dart files:
import 'package:flutter_mvu/mvu.dart'; 
Compatibility: Supports Dart ≥2.17 and Flutter ≥3.0 with full null-safety.
💡 Concept Overview #
At the heart of MVU are four simple ideas:
- Model: Your app's state – any plain Dart object holding data; no base class or mixin required.
 - Event: A message describing what happened (user tap, data fetched, etc.).
 - Update: The 
updateModelmethod inside yourEvent<T>implementation—where you define how theModelchanges in response to theEvent. - View: A Flutter widget that renders the current 
Modeland emitsEvent<T>events via the providedtriggerEventcallback. 
Unidirectional flow:
User Interaction ➡️ Event ➡️ updateModel ➡️ Model Updated ➡️ View Rebuild ➡️ ...
This clear flow ensures that all state changes are predictable, easy to trace, and simple to test. 🛤️🔍
📝 API Summary 📋 #
🔸 ModelController #
Manages a model instance, processes events, emits states, and optionally dispatches initial events.
class ModelController<T extends Object> {
  ModelController(
    T model, {
    List<Event<T>> initialEvents = const [],
  });
  T get model;
  Stream<T> get stream;
  Stream<OutEvent<T>> get outEventStream;
  void triggerEvent(Event<T> event);
  void notifyListeners();
  void dispose();
}
- 
Constructor:
ModelController(model)— no initial events.ModelController(model, initialEvents: [...])— enqueues those right after initialization.
 - 
Properties:
model: the current state instance.stream: a broadcast stream of state snapshots.outEventStream: a broadcast stream ofOutEvent<T>for parent-child communication.
 - 
Methods:
triggerEvent(event): enqueue anEvent<T>for processing.notifyListeners(): manually emit the current model intostream.dispose(): close all internal streams and free resources.
 
🔸 Event #
Defines how to update the Model when something happens.
abstract class Event<T> {
  void updateModel(
    T model,
    void Function(Event<T>) triggerEvent,
    void Function(OutEvent<T>) triggerOutEvent,
  );
}
- Implement 
Event<T>and overrideupdateModelto update the model. - Use 
triggerEventto chain further events. - Use 
triggerOutEventto bubble messages to parent models. - Add attributes to the Event, which are being set by the constructor, to create parameterized events
 
🔸 OutEvent #
Marker for bubbling child → parent messages.
abstract class OutEvent<T> {}
Emit via triggerOutEvent(...) inside updateModel.
Parent-Child Wiring Example
class ParentModel { /*…*/ }
class ChildModel  { /*…*/ }
final childCtrl = ModelController(ChildModel());
final parentCtrl = ModelController(ParentModel());
// In parent’s setup logic:
childCtrl.outEventStream.listen((out) {
  parentCtrl.triggerEvent(ChildDidSomething(out.info));
});
class ChildDidSomething extends OutEvent<MyChildModel> {
  final String info;
  ChildDidSomething(this.info);
}
🔸 StateView #
Defines how to render UI for a given state.
abstract class StateView<T> {
  Widget view(
    BuildContext context,
    T currentState,
    void Function(Event<T>) triggerEvent,
  );
}
- Build pure functions: no internal state, just 
context,state,triggerEvent. 
🔸 ModelProvider #
A StatefulWidget that binds a ModelController<T> to a StateView<T>.
- 
Auto-managed constructor:
ModelProvider( MyModel(), // your raw model stateView: MyView(), // your StateView implementation initialEvents: [], // optional list of initial events to be triggered after model initialization )• Creates its own
ModelControllerand auto-disposes it. - 
Self-managed constructor:
ModelProvider.controller( myController, // existing ModelController stateView: MyView(), )• Uses your controller and does not dispose it; you manage lifecycle.
 
⚠️ For self-managed controllers, remember to call controller.dispose() when you’re done to avoid memory leaks.
🚀 Examples #
1️⃣ Counter Example (Auto-managed) #
// 1️⃣ Define the Model
class CounterModel {
  int count = 0;
}
// 2️⃣ Define an Event
class IncrementEvent implements Event<CounterModel> {
  @override
  void updateModel(CounterModel model, triggerEvent, _) {
    model.count++;
  }
}
// 3️⃣ Define the View
class CounterView extends StateView<CounterModel> {
  @override
  Widget view(BuildContext context, CounterModel state, triggerEvent) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Count: ${state.count}', style: TextStyle(fontSize: 32)),
          ElevatedButton(
            onPressed: () => triggerEvent(IncrementEvent()),
            child: Text('Increment ➕'),
          ),
        ],
      ),
    );
  }
}
// 4️⃣ Wire up in main()
void main() {
  runApp(MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: Text('Auto-managed Counter')),
      body: ModelProvider(
        CounterModel(),
        stateView: CounterView(),
      ),
    ),
  ));
}
2️⃣ Counter Example (Self-managed) #
// Reuse CounterModel, IncrementEvent, CounterView from above
void main() {
  final counterController = ModelController(CounterModel());
  runApp(MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: Text('Self-managed Counter')),
      body: ModelProvider.controller(
        counterController,
        stateView: CounterView(),
      ),
    ),
  ));
}
3️⃣ Counter (Initial Events) #
final provider = ModelProvider(
  CounterModel(),
  initialEvents: [IncrementEvent(), IncrementEvent()],
  stateView: CounterView(),
);
Immediately, the counter starts at 2!
4️⃣ Async Event Pattern ⏳ #
// 1️⃣ Model with loading/error state
class DataModel {
  bool isLoading = false;
  List<String>? items;
  String? error;
}
// 2️⃣ Define result events
class DataLoadedEvent implements Event<DataModel> {
  final List<String> items;
  DataLoadedEvent(this.items);
  @override
  void updateModel(DataModel model, _, __) {
    model.items = items;
    model.isLoading = false;
  }
}
class DataLoadFailedEvent implements Event<DataModel> {
  final String message;
  DataLoadFailedEvent(this.message);
  @override
  void updateModel(DataModel model, _, __) {
    model.error = message;
    model.isLoading = false;
  }
}
// 3️⃣ Async fetch event
class FetchDataEvent implements Event<DataModel> {
  @override
  void updateModel(DataModel model, triggerEvent, _) {
    model.isLoading = true;
    fetchRemoteItems()
      .then((items) => triggerEvent(DataLoadedEvent(items)))
      .catchError((err) => triggerEvent(DataLoadFailedEvent(err.toString())));
  }
}
🧪 Testing 🧪 #
For unit and widget tests, add the flutter_mvu_test package to your dev dependencies:
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_mvu_test: ^1.0.1
Then import in your test files:
import 'package:flutter_mvu_test/flutter_mvu_test.dart';
Use TestModelController<T> to synchronously dispatch events and assert on both model state, Event<T> and OutEvent<T> emissions.
🎓 Tips & Next Steps #
- Dispose wisely: Auto-managed providers handle it for you; for self-managed, call 
dispose()when appropriate. - OutEvents: Implement to communicate child→parent updates in nested models.
 - Debug logs: In debug builds, events are printed automatically for easy tracing.
 
Happy MVU‑ing! 🚀🎨✨