flutter_mvu 1.0.0 copy "flutter_mvu: ^1.0.0" to clipboard
flutter_mvu: ^1.0.0 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 🔧 #

  1. Add to pubspec.yaml:
    dependencies:
      flutter:
        sdk: flutter
      flutter_mvu: ^0.1.0
    
  2. Fetch packages:
    flutter pub get
    
  3. 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:

  1. Model: Your app's state – any plain Dart object holding data; no base class or mixin required.
  2. Event: A message describing what happened (user tap, data fetched, etc.).
  3. Update: The updateModel method inside your Event<T> implementation—where you define how the Model changes in response to the Event.
  4. View: A Flutter widget that renders the current Model and emits Event<T> events via the provided triggerEvent callback.

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 your Model instance, processes Event<T> events, and broadcasts state changes.

  • Constructor: ModelController(T initialModel)
  • Properties:
    • T get model – latest state object
    • Stream<T> get stream – broadcast of state snapshots
  • Methods:
    • void triggerEvent(Event<T> event) – enqueue an event
    • void dispose() – close 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 override updateModel.
  • Use triggerEvent to chain further events.
  • Use triggerOutEvent to bubble messages to parent models.

🔸 OutEvent #

A simple marker to bubble child→parent messages. Your model can be any class; no inheritance needed.

class ChildDidSomething extends OutEvent<MyChildModel> {
  final String info;
  ChildDidSomething(this.info);
}

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 a Model and emit Event<T> events in response.

abstract class StateView<T> {
  Widget view(
    BuildContext context,
    T currentState,
    void Function(Event<T>) 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
    )
    

    • Creates its own ModelController and 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️⃣ 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 Example 🧪 #

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_mvu/mvu.dart';
import 'path/to/test_model_controller.dart';

class CounterModel { int count = 0; }
class IncrementEvent implements Event<CounterModel> {
  @override
  void updateModel(CounterModel model, _, __) => model.count++;
}

test('increment via dispatch()', () async {
  final testController = TestModelController(CounterModel());
  await testController.dispatch(IncrementEvent());
  expect(testController.model.count, 1);
});

🎓 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! 🚀🎨✨

1
likes
0
points
23
downloads

Publisher

unverified uploader

Weekly Downloads

A minimal Elm‑inspired Model‑View‑Update state management library for Flutter.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_mvu