minix 1.2.0 copy "minix: ^1.2.0" to clipboard
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 container
  • Observer: Widget that rebuilds when observables change
  • ContextObserver: Observer with BuildContext access
  • ListObservable<T>: Reactive list implementation
  • MapObservable<K,V>: Reactive map implementation
  • FormObservable: Form management with validation
  • AsyncObservable<T>: Async operation state management
  • StreamObservable<T>: Stream integration with reactive state
  • ComputedObservable<T>: Derived values that auto-update
  • Injector: Dependency injection container
  • AutoDispose: Automatic resource cleanup mixin

Helper Classes #

  • ActionController: Batched state updates
  • Selector<T,R>: Derived observable values
  • Injectable: Base class for injectable services

πŸ“„ License #

This project is licensed under the MIT License - see the LICENSE file for details.

⭐ Show your support #

Give a ⭐️ if this project helped you!


Made with ❀️ by the Jai Prakash Thawait

2
likes
130
points
33
downloads

Publisher

unverified uploader

Weekly Downloads

Reactive state management for Flutter with form validation, collection observables, async operations, dependency injection, and automatic memory management. Zero dependencies, high performance, and comprehensive testing support for scalable applications.

Repository (GitHub)
View/report issues

Topics

#state-management #dependency-injection #reactive-programming #flutter #observables

Documentation

API reference

License

MIT (license)

Dependencies

flutter, intl

More

Packages that depend on minix