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