mastro 0.9.6
mastro: ^0.9.6 copied to clipboard
Transform Flutter state management into an art form with Mastro the elegant conductor of your app's data symphony
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 #
- Key Features
- Installation
- 1. Initialization
- 2. State Management
- 3. Persistent Storage
- 4. MastroBox Pattern
- 5. BoxProviders
- 6. Event Handling
- 7. Widget Building
- 8. MastroView Pattern
- 9. Scopes
- Project Structure
- Examples
- Contributions
- 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
MastroBoxto 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
MastroBoxinstance to the widget tree. - Usage: Use
BoxProviderwhen you need to provide a single box to a subtree. - Example:
BoxProvider<NotesBox>( create: (_) => NotesBox(), child: NotesView(), );
MultiBoxProvider
- Purpose: Provides multiple
MastroBoxinstances to the widget tree. - Usage: Use
MultiBoxProviderwhen 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
MastroEventand implement theimplementmethod. - 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
MastroBuilderto 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.tagto trigger updates for specific tags. - Usage: Use
TagBuilderto 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
MastroViewto 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
OnPopScopeto 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
addEventBlockPopto 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
MultiBoxProviderto defineMastroBoxinstances 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
MastroBoxinstance directly to aMastroViewfor 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.