Mastro
Mastro is a state management solution for Flutter that combines reactive programming with event handling and persistence. It provides a structured way to manage state, handle events, and persist data across app sessions.
Table of Contents
- 1. Key Features
- 2. Installation
- 3. Initialization
- 4. State Management
- 5. Persistent Storage
- 6. MastroBox Pattern
- 7. BoxProviders
- 8. Event Handling
- 9. Widget Building
- 10. MastroView Pattern
- 11. Scopes
- 12. Project Structure
- 13. Examples
- 14. Contributions
- 15. License
Key Features
- π― Simple State Management - Lightweight and Mastro state objects
- π Reactive Updates - Efficient widget rebuilding
- πΎ Persistent Storage - Built-in persistence capabilities
- π¦ MastroBox Pattern - Organized business logic and state
- π Event Handling - Structured event processing
- π Debug Tools - Built-in debugging capabilities
- ποΈ Builder Widgets - Flexible widget building
- π State Validation - Input validation support
- π Computed States - Derived values with automatic updates
- π― Event Modes - Parallel, Sequential, and Solo event processing
- π Lifecycle Management - Built-in lifecycle hooks
- π¨ UI Patterns - Structured view and widget patterns
Installation
Add the following to your pubspec.yaml
file:
dependencies:
mastro: <latest_version>
Then, run flutter pub get
to install the package.
1. Initialization
To use Mastro, you need to initialize it in your main.dart
file. This setup ensures that all necessary components are ready before your app starts.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await MastroInit.initialize(); // Initialize Mastro
...
runApp(MaterialApp(home: MastroScope(child: YourHomeWidget())));
}
2. State Management
Mastro offers two primary ways to manage state: Lightro
and Mastro
. Both can handle any state, but Mastro
provides additional features.
Lightro - Simple State
- Purpose: Manage states with a straightforward approach.
- Usage: Ideal for basic state management where you need to track a single value.
- Example:
final counter = 0.lightro; // Create a simple state // Update the state counter.value++; // Reactive UI with MastroBuilder MastroBuilder( state: counter, builder: (state, context) => Text('Counter: ${state.value}'), );
Mastro - Advanced State
- Purpose: Manage states with additional features like dependencies, computed values, and validation.
- Usage: Suitable for scenarios where state changes depend on other states or require validation.
- Example:
class User { String name; int age; User({required this.name, required this.age}); } final user = User(name: 'Alice', age: 30).mastro; // Create a complex state // Modify the state without replacing the object user.modify((state) { state.value.name = 'Bob'; state.value.age = 31; }); // Reactive UI with MastroBuilder MastroBuilder( state: user, builder: (state, context) => Column( children: [ Text('Name: ${state.value.name}'), Text('Age: ${state.value.age}'), ], ), );
Mastro Functions
-
dependsOn: Establish dependencies between states. When the dependent state changes, the current state is notified.
dependentState.dependsOn(anotherState);
-
compute: Define computed values based on other states. Automatically updates when the source states change.
final someState = 10.mastro; final computedState = someState.compute((value) => value * 5);
-
setValidator: Set validation logic for a state. Ensures that the state value meets certain criteria.
final validatedState = 2.mastro; validatedState.setValidator((value) => value > 0); validatedState.value = 1; // this will be accepted validatedState.value = -1; // this will be ignored
-
observe: Observe changes in the state and execute a callback when the state changes.
observedState.observe((value) { print('State changed to $value'); });
Differences Between Lightro and Mastro
Feature | Lightro | Mastro |
---|---|---|
Modify method | β | β |
Dependencies | β | β |
Computed states | β | β |
Validation | β | β |
Observers | β | β |
3. Persistent Storage
Persistro Class
- Purpose: Provides a base for persistent storage using
SharedPreferences
. - Usage: Can be used directly for custom persistence logic.
- Example:
// Direct usage example Future<void> saveCustomData(String key, String value) async { await Persistro.putString(key, value); } Future<String?> loadCustomData(String key) async { return await Persistro.getString(key); }
PersistroMastro and PersistroLightro
These classes extend the functionality of Mastro
and Lightro
by adding persistence capabilities, allowing state data to be saved and restored across app sessions.
-
PersistroLightro:
- Purpose: Manage simple, single-value states with persistence.
- Usage: Ideal for persisting basic settings or preferences.
- Example:
final isDarkMode = PersistroLightro.boolean('isDarkMode', initial: false); // Persistent boolean state // Toggle dark mode isDarkMode.toggle(); // Reactive UI with MastroBuilder MastroBuilder( state: isDarkMode, builder: (state, context) => Text('Dark Mode: ${state.value ? "On" : "Off"}'), );
-
PersistroMastro:
- Purpose: Manage complex states with persistence, including lists and maps.
- Usage: Suitable for persisting collections or objects with multiple properties.
- Example:
final notes = PersistroMastro.list<Note>( 'notes', initial: [], fromJson: (json) => Note.fromJson(json), ); // Add a new note notes.modify((state) { state.value.add(Note( id: '1', title: 'New Note', content: 'This is a new note.', createdAt: DateTime.now(), )); }); // Reactive UI with MastroBuilder MastroBuilder( state: notes, builder: (state, context) => ListView.builder( itemCount: state.value.length, itemBuilder: (context, index) { final note = state.value[index]; return ListTile(title: Text(note.title)); }, ), );
4. MastroBox Pattern
MastroBox is the core container for your application's state and logic.
- Purpose: Organize state and business logic in a structured way.
- Usage: Extend
MastroBox
to create a container for your app's state and logic. - Example:
class NotesBox extends MastroBox<NotesEvent> { final notes = PersistroMastro.list<Note>( 'notes', initial: [], fromJson: (json) => Note.fromJson(json), ); @override void init() { notes.debugLog(); } }
5. BoxProviders
BoxProvider
and MultiBoxProvider
are used to manage the lifecycle of MastroBox
instances and provide them to the widget tree.
BoxProvider
- Purpose: Provides a single
MastroBox
instance to the widget tree. - Usage: Use
BoxProvider
when you need to provide a single box to a subtree. - Example:
BoxProvider<NotesBox>( create: (_) => NotesBox(), child: NotesView(), );
MultiBoxProvider
- Purpose: Provides multiple
MastroBox
instances to the widget tree. - Usage: Use
MultiBoxProvider
when you need to provide multiple boxes to a subtree. - Example:
MultiBoxProvider( providers: [ BoxProvider(create: (_) => NotesBox()), BoxProvider(create: (_) => AnotherBox()), ], child: MyApp(), );
6. Event Handling
Events in Mastro provide a structured way to handle actions and state changes.
- Purpose: Define and handle events that modify the state.
- Usage: Create event classes that extend
MastroEvent
and implement theimplement
method. - Example:
sealed class NotesEvent extends MastroEvent<NotesBox> { const NotesEvent(); factory NotesEvent.add(String title, String content) = _AddNoteEvent; } class _AddNoteEvent extends NotesEvent { final String title; final String content; _AddNoteEvent(this.title, this.content); @override Future<void> implement(NotesBox box, Callbacks callbacks) async { final note = Note( id: DateTime.now().millisecondsSinceEpoch.toString(), title: title, content: content, createdAt: DateTime.now(), ); box.notes.modify((notes) => notes.value.add(note)); // Notify listeners that note was added successfully callbacks.invoke('onNoteAdded', data: {'noteId': note.id}); } } // Using callbacks when adding event await box.addEvent( NotesEvent.add('Title', 'Content'), callbacks: Callbacks({ 'onNoteAdded': ({data}) { print('Note added with ID: ${data?['noteId']}'); }, }), );
Event Modes
class ComplexEvent extends MastroEvent<AppBox> {
@override
EventRunningMode get mode => EventRunningMode.sequential;
// Available modes:
// - parallel (default): Multiple instances can run simultaneously
// - sequential: Events of same type are queued
// - solo: Only one instance can run at a time
}
7. Widget Building
Mastro provides builder widgets to create reactive UIs.
MastroBuilder
- Purpose: Build widgets that automatically update when the state changes.
- Usage: Use
MastroBuilder
to wrap widgets that depend on a state. - Parameters:
state
: The state object that the widget depends on.builder
: A function that builds the widget based on the current state.listeners
(optional): A list of additional state objects to listen to. If any of these states change, the widget will rebuild.shouldRebuild
(optional): A function that determines whether the widget should rebuild when the state changes. It takes the previous and current state values as arguments and returns a boolean. If not provided, the widget will rebuild on every state change.
- Example:
MastroBuilder( state: counter, builder: (state, context) => Text('Counter: ${state.value}'), );
TagBuilder
- Purpose: Rebuild parts of the UI by calling
box.tag
to trigger updates for specific tags. - Usage: Use
TagBuilder
to create widgets that needs to be rebuild when a specific tag is triggered. - Example:
TagBuilder( tag: 'important', box: box, builder: (context) => Text('This is an important update!'), ); // Trigger a rebuild for the 'important' tag box.tag('important');
8. MastroView Pattern
MastroView provides a structured way to create screens with lifecycle management.
- Purpose: Manage the lifecycle of a screen and its associated state.
- Usage: Extend
MastroView
to create a screen with lifecycle hooks.
Using Local Box
Create or pass a MastroBox
instance directly to a MastroView
super constructor.
- Example:
class LocalNotesView extends MastroView<NotesBox> { LocalNotesView({super.key}) : super(box: NotesBox()); @override Widget build(BuildContext context, NotesBox box) { return Scaffold( appBar: AppBar(title: const Text('Local Notes')), body: MastroBuilder( state: box.notes, builder: (notes, context) => ListView.builder( itemCount: notes.value.length, itemBuilder: (context, index) { final note = notes.value[index]; return ListTile(title: Text(note.title)); }, ), ), ); } }
Using BoxProvider
Use MultiBoxProvider
or BoxProvider
to define MastroBox
instances in the widget tree prior to the creation of the MastroView.
- Example:
class GlobalNotesView extends MastroView<NotesBox> { const GlobalNotesView({super.key}); @override Widget build(BuildContext context, NotesBox box) { return Scaffold( appBar: AppBar(title: const Text('Global Notes')), body: MastroBuilder( state: box.notes, builder: (notes, context) => ListView.builder( itemCount: notes.value.length, itemBuilder: (context, index) { final note = notes.value[index]; return ListTile(title: Text(note.title)); }, ), ), ); } }
9. Scopes
Mastro provides a way to manage app-wide behaviors using scopes, particularly useful when handling events that block user interactions.
OnPopScope
- Purpose: Manage user interactions during blocking events within
MastroScope
. - Usage: Use
OnPopScope
to define behavior when an event blocks user interactions. - Example:
MaterialApp( home: MastroScope( onPopScope: OnPopScope( onPopWaitMessage: (context) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please wait...')), ); }, ), child: YourHomeWidget(), ), );
addEventBlockPop
- Purpose: Execute events that block user interactions until completion.
- Usage: Use
addEventBlockPop
to run events that should prevent user actions until they finish. - Example:
await box.addEventBlockPop( context, NotesEvent.add('New Note', 'This is a new note'), );
Global vs. Local Box Usage
-
Global Usage: Use
MultiBoxProvider
to defineMastroBox
instances that can be accessed from anywhere in the app. This is useful for app-wide settings or data that needs to be shared across multiple screens. -
Local Usage: Pass a
MastroBox
instance directly to aMastroView
for data that is only relevant to a particular screen or widget.
Project Structure
Mastro follows a feature-based architecture pattern that promotes organization and separation of concerns. Here's the recommended project structure:
lib/
βββ core/ # Core functionality and configurations
βββ shared/ # Shared resources (models, utilities, etc.)
β βββ models/
βββ features/ # Feature modules
βββ notes/ # Example feature
βββ logic/
β βββ notes_box.dart
β βββ notes_events.dart
βββ presentation/
βββ components/ # Feature-specific widgets
βββ notes_view.dart
Feature Structure Explanation
Each feature follows a consistent structure:
-
Logic Layer (
logic/
)*_box.dart
: Contains the MastroBox implementation for the feature*_events.dart
: Defines feature-specific events
-
Presentation Layer (
presentation/
)*_view.dart
: Main view implementation using MastroViewcomponents/
: Feature-specific widgets and UI components
Example Feature Implementation
// features/notes/logic/notes_box.dart
class NotesBox extends MastroBox<NotesEvent> {
final notes = PersistroMastro.list<Note>('notes', initial: []);
}
// features/notes/logic/notes_events.dart
sealed class NotesEvent extends MastroEvent<NotesBox> {
const NotesEvent();
factory NotesEvent.add(Note note) = _AddNoteEvent;
}
// features/notes/presentation/notes_view.dart
class NotesView extends MastroView<NotesBox> {
const NotesView({super.key});
@override
Widget build(BuildContext context, NotesBox box) {
return Scaffold(
appBar: AppBar(title: const Text('Notes')),
body: MastroBuilder(
state: box.notes,
builder: (notes, context) => NotesListView(notes: notes.value),
),
);
}
}
This structure promotes:
- Clear separation of concerns
- Feature isolation
- Easy navigation and maintenance
- Scalable architecture
- Reusable components
Examples
Check the example
folder for more detailed examples of how to use Mastro in your Flutter app.
Contributions
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request on GitHub.
License
This project is licensed under the MIT License - see the LICENSE file for details.