mastro 0.9.1
mastro: ^0.9.1 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.
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(MastroScope(child: MaterialApp(home: 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 creating a new 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.
final dependentState = someState.mastro; dependentState.dependsOn(anotherState); -
compute: Define computed values based on other states. Automatically updates when the source states change.
final computedState = someState.mastro; computedState.compute(() => anotherState.value * 2); -
setValidator: Set validation logic for a state. Ensures that the state value meets certain criteria.
final validatedState = someState.mastro; validatedState.setValidator((value) => value > 0); -
observe: Observe changes in the state and execute a callback when the state changes.
final observedState = someState.mastro; observedState.observe((value) { print('State changed to $value'); });
Differences Between Lightro and Mastro
| Feature | Lightro | Mastro |
|---|---|---|
| Dependencies | β | β |
| Computed values | β | β |
| Validation | β | β |
| Observers | β | β |
| Modify method | β | β |
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)); } }
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. - 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', 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
- 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
- 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 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.
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.