minix 1.2.0
minix: ^1.2.0 copied to clipboard
Reactive state management for Flutter with form validation, collection observables, async operations, dependency injection, and automatic memory management. Zero dependencies, high performance, and co [...]
Minix π - Reactive State Management for Flutter #
A powerful, lightweight, and intuitive reactive state management library for Flutter and Dart applications. Minix provides a comprehensive set of tools for managing application state with automatic reactivity, dependency injection, and memory management.
π Features #
- π Reactive Observables: Automatic UI updates when state changes
- π Form Management: Built-in reactive forms with validation
- π Collection Observables: Reactive lists and maps with automatic updates
- π Async Support: Built-in async operations handling
- π‘ Stream Integration: Reactive stream management
- π§ ComputedObservable: for derived state that auto-updates when dependencies change.
- β‘οΈ runInAction: to batch multiple updates into a single reactive transaction.
- π Dependency Injection: Simple and powerful IoC container
- π§Ή Auto Disposal: Automatic memory management to prevent leaks
- β‘ High Performance: Optimized for minimal overhead and maximum efficiency
- π― Type Safe: Full TypeScript-like type safety for Dart
- π§ͺ Test Friendly: Built-in mocking support for easy testing
- π± Flutter Optimized: Designed specifically for Flutter widgets
- β‘ Performance Optimized: Minimal rebuilds, maximum efficiency
π¦ Installation #
Add minix to your pubspec.yaml
:
dependencies:
minix: ^1.2.0
Then run:
flutter pub get
π Quick Start #
Basic Observable #
import 'package:minix/minix.dart';
// Create an observable
final counter = Observable(0);
// Use in a widget
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Observer(() {
return Text('Count: ${counter.watch()}');
});
}
}
// Update the value
//counter.value = counter.value + 1;
With Context #
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
final count = counter.watch();
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
});
}
}
π Form Management #
Reactive Forms with Validation #
// Create form instance globally or in a controller
final loginForm = FormObservable()
..addField(
'email',
initialValue: '',
validator: (value) {
if (value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Invalid email format';
return null;
},
)
..addField(
'password',
initialValue: '',
validator: (value) {
if (value.isEmpty) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
return null;
},
);
class LoginForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
return Column(
children: [
// Email field
TextField(
onChanged: (value) => loginForm.setValue('email', value),
decoration: InputDecoration(
labelText: 'Email',
errorText: loginForm.fieldError('email').watch(),
),
),
// Password field
TextField(
onChanged: (value) => loginForm.setValue('password', value),
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: loginForm.fieldError('password').watch(),
),
),
// Submit button
ElevatedButton(
onPressed: loginForm.watchIsValid() ? _handleSubmit : null,
child: Text('Login'),
),
],
);
});
}
void _handleSubmit() {
final values = loginForm.getValues();
print('Email: ${values['email']}');
print('Password: ${values['password']}');
// You can also reset the form after submission
// loginForm.reset();
}
}
π List Management #
Reactive Lists #
// Create the list observable globally or in a controller
final todos = ListObservable<String>([
'Learn Flutter',
'Build awesome apps',
'Master Minix',
]);
class TodoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
final todoList = todos.watch();
return Column(
children: [
Text('Total todos: ${todos.watchLength()}'),
Expanded(
child: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todoList[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => todos.removeAt(index),
),
);
},
),
),
ElevatedButton(
onPressed: () => _addTodo(),
child: Text('Add Todo'),
),
],
);
});
}
void _addTodo() {
todos.add('New todo ${todos.length + 1}');
}
}
Advanced List Operations #
final numbers = ListObservable<int>([1, 2, 3, 4, 5]);
// Filter reactive list
final evenNumbers = numbers.where((n) => n % 2 == 0);
// Transform reactive list
final doubledNumbers = numbers.map((n) => n * 2);
// Watch specific properties
Observer(() {
return Column(
children: [
Text('Count: ${numbers.watchLength()}'),
Text('Is empty: ${numbers.watchIsEmpty()}'),
Text('First: ${numbers.watchFirst() ?? 'None'}'),
Text('Last: ${numbers.watchLast() ?? 'None'}'),
],
);
});
// Bulk operations
numbers.addAll([6, 7, 8]);
numbers.removeWhere((n) => n > 5);
numbers.sort();
numbers.clear();
πΊοΈ Map Management #
Reactive Maps #
// Create the map observable globally or in a controller
final userProfile = MapObservable<String, String>({
'name': 'John Doe',
'email': 'john@example.com',
'role': 'Developer',
});
class UserProfileManager extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
final profile = userProfile.watch();
return Column(
children: [
Text('Profile has ${userProfile.length} fields'),
...profile.entries.map((entry) => ListTile(
title: Text(entry.key),
subtitle: Text(entry.value),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () => _editField(entry.key, entry.value),
),
)),
ElevatedButton(
onPressed: () => _addField(),
child: Text('Add Field'),
),
],
);
});
}
void _editField(String key, String currentValue) {
// Show dialog to edit field
userProfile.put(key, '$currentValue (edited)');
}
void _addField() {
userProfile.put('newField', 'New Value');
}
}
π Async Operations #
Handling Async State #
// Create the async observable globally or in a controller
final dataLoader = AsyncObservable<List<String>>();
class DataLoader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
final state = dataLoader.watchState();
final data = dataLoader.watchData();
final error = dataLoader.watchError();
return Column(
children: [
// Load data button
ElevatedButton(
onPressed: () => _loadData(),
child: Text('Load Data'),
),
SizedBox(height: 20),
// State-based UI
_buildContent(state, data, error),
],
);
});
}
Widget _buildContent(AsyncState state, List<String>? data, String? error) {
switch (state) {
case AsyncState.loading:
return Center(child: CircularProgressIndicator());
case AsyncState.error:
return Column(
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => _loadData(),
child: Text('Retry'),
),
],
);
case AsyncState.success:
return Expanded(
child: ListView.builder(
itemCount: data?.length ?? 0,
itemBuilder: (context, index) {
return ListTile(title: Text(data![index]));
},
),
);
default:
return Text('Press "Load Data" to start');
}
}
void _loadData() {
dataLoader.execute(() async {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
// Simulate random failure
if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {
throw Exception('Network error');
}
return ['Item 1', 'Item 2', 'Item 3'];
});
}
}
π Stream Integration #
Working with Streams #
// Create the stream observable globally or in a controller
final messageStream = StreamObservable<String>();
class ChatMessages extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
final state = messageStream.watchState();
final message = messageStream.watchData();
final hasData = messageStream.watchHasData();
return Column(
children: [
Text('Stream State: ${state.toString()}'),
if (hasData)
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(message ?? 'No message'),
),
),
Row(
children: [
ElevatedButton(
onPressed: () => _startListening(),
child: Text('Start'),
),
ElevatedButton(
onPressed: messageStream.isListening ? messageStream.pause : null,
child: Text('Pause'),
),
ElevatedButton(
onPressed: messageStream.isPaused ? messageStream.resume : null,
child: Text('Resume'),
),
],
),
],
);
});
}
void _startListening() {
// Create a stream (e.g., from WebSocket or Firebase)
final stream = Stream.periodic(
Duration(seconds: 2),
(count) => 'Message ${count + 1}',
);
messageStream.listen(stream);
}
}
π Dependency Injection #
Service Registration and Usage #
// Define a service
class ApiService extends Injectable {
@override
void onInit() {
print('ApiService initialized');
}
Future<List<String>> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return ['Data 1', 'Data 2', 'Data 3'];
}
@override
void onDispose() {
print('ApiService disposed');
}
}
// Register services
void main() {
// Register singleton
Injector.put(ApiService());
// Register lazy singleton
Injector.lazyPut(() => ApiService());
// Register with scope
Injector.putScoped(ApiService(), 'user_session');
// Register with tag
Injector.putTagged(ApiService(), 'primary_api');
runApp(MyApp());
}
// Use in widgets
class DataWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
// Get service instance
final apiService = Injector.find<ApiService>();
return FutureBuilder<List<String>>(
future: apiService.fetchData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data!.map((item) => ListTile(
title: Text(item),
)).toList(),
);
}
return CircularProgressIndicator();
},
);
});
}
}
// Clean up when needed
void cleanupUserSession() {
Injector.disposeScope('user_session');
}
π― Computed Values #
Derived State #
// Create observables globally or in a controller
final items = ListObservable<CartItem>();
final taxRate = Observable(0.08); // 8% tax
// Computed values automatically update when dependencies change
final subtotal = ComputedObservable(
() => items.value.fold(0.0, (sum, item) => sum + (item.price * item.quantity)),
[items],
);
final tax = ComputedObservable(
() => subtotal.value * taxRate.value,
[subtotal, taxRate],
);
final total = ComputedObservable(
() => subtotal.value + tax.value,
[subtotal, tax],
);
class ShoppingCart extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContextObserver((context) {
return Column(
children: [
Text('Items: ${items.watchLength()}'),
Text('Subtotal: \${subtotal.watch().toStringAsFixed(2)}'),
Text('Tax: \${tax.watch().toStringAsFixed(2)}'),
Text('Total: \${total.watch().toStringAsFixed(2)}',
style: TextStyle(fontWeight: FontWeight.bold)),
// Items list
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item?.name ?? ''),
subtitle: Text('\${item?.price.toStringAsFixed(2)} x ${item?.quantity}'),
trailing: IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => items.removeAt(index),
),
);
},
),
),
ElevatedButton(
onPressed: () => _addItem(),
child: Text('Add Item'),
),
],
);
});
}
void _addItem() {
items.add(CartItem('Item ${items.length + 1}', 10.0, 1));
}
}
class CartItem {
final String name;
final double price;
final int quantity;
CartItem(this.name, this.price, this.quantity);
}
π§ͺ Testing Support #
Mocking Observables #
import 'package:flutter_test/flutter_test.dart';
import 'package:minix/minix.dart';
void main() {
group('Counter Tests', () {
late Observable<int> counter;
setUp(() {
counter = Observable(0);
});
tearDown(() {
counter.dispose();
});
test('should increment counter', () {
expect(counter.value, 0);
counter.value = 1;
expect(counter.value, 1);
});
test('should notify listeners on change', () {
var notified = false;
counter.addListener(() => notified = true);
counter.value = 5;
expect(notified, true);
});
});
}
π οΈ Advanced Configuration #
Custom Equality and Error Handling #
// Custom equality function
final userObservable = Observable(
User('John', 25),
equals: (a, b) => a.name == b.name && a.age == b.age,
);
// Global error handling
void main() {
// Set global error handler for actions
ActionController.setDefaultErrorHandler((error, stackTrace) {
print('Global error: $error');
// Log to crash reporting service
});
// Enable injection logging
Injector.enableLogs = true;
runApp(MyApp());
}
Performance Optimization #
class OptimizedWidget extends StatelessWidget {
final Observable<int> counter = Observable(0);
@override
Widget build(BuildContext context) {
return Observer(
() => Text('Count: ${counter.watch()}'),
// Only rebuild when certain conditions are met
shouldRebuild: () => counter.value % 2 == 0,
);
}
}
// For better performance with StatelessWidget, you can also use controllers
class CounterController {
final counter = Observable(0);
void increment() => counter.value++;
void decrement() => counter.value--;
void dispose() => counter.dispose();
}
// Register controller globally or with dependency injection
final counterController = CounterController();
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Observer(() {
return Column(
children: [
Text('Count: ${counterController.counter.watch()}'),
Row(
children: [
ElevatedButton(
onPressed: counterController.increment,
child: Text('+'),
),
ElevatedButton(
onPressed: counterController.decrement,
child: Text('-'),
),
],
),
],
);
});
}
}
π API Reference #
Core Classes #
Observable<T>
: Basic reactive value containerObserver
: Widget that rebuilds when observables changeContextObserver
: Observer with BuildContext accessListObservable<T>
: Reactive list implementationMapObservable<K,V>
: Reactive map implementationFormObservable
: Form management with validationAsyncObservable<T>
: Async operation state managementStreamObservable<T>
: Stream integration with reactive stateComputedObservable<T>
: Derived values that auto-updateInjector
: Dependency injection containerAutoDispose
: Automatic resource cleanup mixin
Helper Classes #
ActionController
: Batched state updatesSelector<T,R>
: Derived observable valuesInjectable
: Base class for injectable services
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π Links #
β Show your support #
Give a βοΈ if this project helped you!
Made with β€οΈ by the Jai Prakash Thawait