reactive_notifier 2.7.3 copy "reactive_notifier: ^2.7.3" to clipboard
reactive_notifier: ^2.7.3 copied to clipboard

A Dart library for managing reactive state efficiently, supporting multiples related state.

ReactiveNotifier #

A flexible, elegant, and secure tool for state management in Flutter. Designed with fine-grained state control in mind, it easily integrates with architectural patterns like MVVM, guarantees full independence from BuildContext, and is suitable for projects of any scale.

reactive_notifier

Dart SDK Version Flutter Platform pub package likes downloads popularity

License: MIT CI

Features #

  • ๐Ÿš€ Simple and intuitive API
  • ๐Ÿ—๏ธ Perfect for MVVM architecture
  • ๐Ÿ”„ Independent from BuildContext
  • ๐ŸŽฏ Type-safe state management
  • ๐Ÿ“ก Built-in Async and Stream support
  • ๐Ÿ”— Smart related states system
  • ๐Ÿ› ๏ธ Repository/Service layer integration
  • โšก High performance with minimal rebuilds
  • ๐Ÿ› Powerful debugging tools
  • ๐Ÿ“Š Detailed error reporting
  • ๐Ÿงน Full lifecycle control with state cleaning
  • ๐Ÿ” Comprehensive state tracking
  • ๐Ÿ“Š Granular state update control

performance_test

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  reactive_notifier: ^2.7.3
copied to clipboard

Core Concepts #

ReactiveNotifier follows a unique "create once, reuse always" approach to state management. Unlike other solutions that recreate state, ReactiveNotifier creates instances on demand and maintains them throughout the app lifecycle. This means:

  • States are created only when needed, optimizing memory usage
  • States persist across the app and can be accessed anywhere
  • Cleanup focuses on resetting state, not destroying instances
  • Organization should be done through mixins, not global variables

Quick Start #

Using ViewModel with ReactiveViewModelBuilder #

The ViewModel approach provides a robust foundation for complex state management with lifecycle hooks and powerful state control.

/// Define your state model
class CounterState {
  final int count;
  final String message;
  
  const CounterState({required this.count, required this.message});
  
  CounterState copyWith({int? count, String? message}) {
    return CounterState(
      count: count ?? this.count,
      message: message ?? this.message
    );
  }
}

/// Define your ViewModel
class CounterViewModel extends ViewModel<CounterState> {
  CounterViewModel() : super(CounterState(count: 0, message: 'Initial'));
  
  @override
  void init() {
    // Initialization logic runs only once when created
    print('Counter initialized');
  }
  
  void increment() {
    transformState((state) => state.copyWith(
      count: state.count + 1,
      message: 'Incremented to ${state.count + 1}'
    ));
  }
  
  void decrement() {
    transformState((state) => state.copyWith(
      count: state.count - 1,
      message: 'Decremented to ${state.count - 1}'
    ));
  }
}


/// Create a mixin to encapsulate your ViewModel
mixin CounterService {
  static final ReactiveNotifierViewModel<CounterViewModel, CounterState> viewModel =
  ReactiveNotifierViewModel(() => CounterViewModel());
}


/// In your UI, use ReactiveViewModelBuilder
class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReactiveViewModelBuilder<CounterState>(
      viewmodel: CounterService.viewModel.notifier,
      builder: (state, keep) {
        return Column(
          children: [
            Text('Count: ${state.count}'),
            Text(state.message),
            
            // Prevent rebuilds with keep
            keep(
              Row(
                children: [
                  ElevatedButton(
                    onPressed: CounterService.viewModel.notifier.decrement,
                    child: Text('-'),
                  ),
                  ElevatedButton(
                    onPressed: CounterService.viewModel.notifier.increment,
                    child: Text('+'),
                  ),
                ],
              ),
            ),
          ],
        );
      },
    );
  }
}
copied to clipboard

Using Simple ReactiveNotifier #

For simpler state management scenarios:

/// Create a mixin to encapsulate state
mixin ThemeService {
  static final ReactiveNotifier<bool> isDarkMode = 
      ReactiveNotifier<bool>(() => false);
  
  static void toggleTheme() {
    isDarkMode.updateState(!isDarkMode.notifier);
  }
}

/// In your UI
class ThemeToggle extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReactiveBuilder<bool>(
      notifier: ThemeService.isDarkMode,
      builder: (isDark, keep) {
        return Switch(
          value: isDark,
          onChanged: (_) => ThemeService.toggleTheme(),
        );
      },
    );
  }
}
copied to clipboard

Lifecycle Management #

Initializing Instances with loadNotifier() #

ReactiveNotifier creates instances on demand, but sometimes you need to preload data at app startup. Use loadNotifier():

// In your main or initialization code
mixin StartupService {
  static Future<void> initializeApp() async {
    // Initialize critical states at startup
    await UserService.userState.loadNotifier();
    await ConfigService.configState.loadNotifier();
    
    runApp(MyApp());
  }
}
copied to clipboard

State Cleaning vs Disposing #

ReactiveNotifier promotes cleaning state rather than disposing instances:

mixin UserService {
  static final userState = ReactiveNotifier<UserModel>(() => UserModel.guest());
  
  // Recommended: Clean state (reset to empty but keep instance)
  static void resetToGuest() {
    userState.cleanCurrentNotifier();
  }
  
  // Alternative: Dispose completely (only if you're certain)
  // Warning: If used elsewhere, this can cause issues
  static void disposeCompletely() {
    ReactiveNotifier.cleanupInstance(userState.keyNotifier);
  }
}

// In a widget's dispose method
@override
void dispose() {
  // If using auto-dispose pattern
  if (widget.cleanOnDispose) {
    UserService.resetToGuest();
  }
  super.dispose();
}
copied to clipboard

State Update Methods #

ReactiveNotifier provides multiple ways to update state with precise control:

updateState and updateSilently #

mixin CounterService {
  static final ReactiveNotifier<int> counter = ReactiveNotifier<int>(() => 0);
  
  // Normal update - triggers widget rebuilds
  static void increment() {
    counter.updateState(counter.notifier + 1);
  }
  
  // Silent update - changes state without rebuilding widgets
  // Useful in initState or for background updates
  static void prepareInitialValue() {
    counter.updateSilently(10);
  }
}
copied to clipboard

transformState and transformStateSilently #

For complex state that needs to be updated based on current values:

mixin CartService {
  static final ReactiveNotifier<CartModel> cart = 
      ReactiveNotifier<CartModel>(() => CartModel.empty());
  
  // Update with notification
  static void addItem(Product product) {
    cart.transformState((state) => state.copyWith(
      items: [...state.items, product],
      total: state.total + product.price
    ));
  }
  
  // Update without notification
  // Useful for background calculations or preparations
  static void prepareCartData(List<Product> products) {
    cart.transformStateSilently((state) => state.copyWith(
      recommendedItems: products,
      // No UI update needed for this background change
    ));
  }
}
copied to clipboard

ReactiveNotifier's related states system allows for managing interdependent states efficiently:

mixin ShopService {
  // Individual state notifiers
  static final ReactiveNotifier<UserState> userState = 
      ReactiveNotifier<UserState>(() => UserState.guest());
  
  static final ReactiveNotifier<CartState> cartState = 
      ReactiveNotifier<CartState>(() => CartState.empty());
  
  static final ReactiveNotifier<ProductsState> productsState = 
      ReactiveNotifier<ProductsState>(() => ProductsState.initial());
  
  // Combined state that's aware of all related states
  static final ReactiveNotifier<ShopState> shopState = ReactiveNotifier<ShopState>(
    () => ShopState.initial(),
    related: [userState, cartState, productsState],
  );
  
  // Access related states in three ways:
  static void showUserCartSummary() {
    // 1. Direct access
    final user = userState.notifier;
    
    // 2. Using from<T>()
    final cart = shopState.from<CartState>();
    
    // 3. Using keyNotifier
    final products = shopState.from<ProductsState>(productsState.keyNotifier);
    
    print("${user.name}'s cart has ${cart.items.length} items with products from ${products.categories.length} categories");
  }
}
copied to clipboard

Special Builder Components #

ReactiveBuilder #

For simple state values:

ReactiveBuilder<bool>(
  notifier: SettingsService.isNotificationsEnabled,
  builder: (enabled, keep) {
    return Switch(
      value: enabled,
      onChanged: (value) => SettingsService.toggleNotifications(),
    );
  },
)
copied to clipboard

ReactiveViewModelBuilder #

For ViewModel-based state:

ReactiveViewModelBuilder<UserProfileData>(
  viewmodel: ProfileService.viewModel.notifier,
  builder: (profile, keep) {
    return Column(
      children: [
        Text(profile.name),
        Text(profile.email),
        keep(ElevatedButton(
          onPressed: ProfileService.viewModel.notifier.logout, 
          child: Text('Logout')
        )),
      ],
    );
  },
)
copied to clipboard

ReactiveAsyncBuilder #

For async operations with loading, error, and success states:

mixin ProductService {
  static final productsViewModel = ReactiveNotifier<ProductsViewModel>(
          () => ProductsViewModel(repository)
  );
}

class ProductsViewModel extends AsyncViewModelImpl<List<Product>> {
  final ProductRepository repository;

  ProductsViewModel(this.repository)
      : super(AsyncState.initial(), loadOnInit: true);

  @override
  Future<List<Product>> loadData() async {
    return await repository.getProducts();
  }

  // Clean state when widget is disposed (don't dispose the ViewModel)
  void onWidgetDispose() {
    cleanState();
  }
}

// In your UI
ReactiveAsyncBuilder<List<Product>>(
  notifier: ProductService.productsViewModel.notifier,
  onSuccess: (products) => ProductGrid(products),
  onLoading: () => CircularProgressIndicator(),
  onError: (error, stack) => Text('Error: $error'),
  onInitial: () => Text('Ready to load products'),
)
copied to clipboard

ReactiveStreamBuilder #

For handling streams with a reactive approach:

mixin ChatService {
  static final messageStream = ReactiveNotifier<Stream<Message>>(
    () => firebaseRepository.getMessageStream()
  );
}

// In your UI
ReactiveStreamBuilder<Message>(
  notifier: ChatService.messageStream,
  onData: (message) => MessageBubble(message),
  onLoading: () => LoadingIndicator(),
  onError: (error) => Text('Error: $error'),
  onEmpty: () => Text('No messages yet'),
  onDone: () => Text('Stream closed'),
)
copied to clipboard

Performance Optimization with keep #

The keep function is a powerful way to prevent unnecessary rebuilds, even for complex widgets containing other reactive components:

ReactiveBuilder<int>(
  notifier: CounterService.counter,
  builder: (count, keep) {
    return Column(
      children: [
        // Rebuilds when count changes
        Text('Count: $count'), 
        
        // These widgets will NOT rebuild when count changes
        keep(
          Image.asset('assets/counter_image.png'),
        ),
        
        // Even another ReactiveBuilder inside keep won't rebuild when the parent rebuilds
        // Only rebuilds when ThemeService.isDarkMode changes
        keep(
          ReactiveBuilder<bool>(
            notifier: ThemeService.isDarkMode,
            builder: (isDark, innerKeep) {
              return Card(
                color: isDark ? Colors.grey[800] : Colors.white,
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    'Theme card with count: $count',
                    style: TextStyle(
                      color: isDark ? Colors.white : Colors.black,
                    ),
                  ),
                ),
              );
            },
          ),
        ),

        const ExpensiveWidget(),

        const ElevatedButton(
          onPressed: CounterService.increment,
          child: const Text('Increment'),
        ),
      ],
    );
  },
)
copied to clipboard

Key points about keep:

  1. Using keep prevents widgets from rebuilding even when their parent rebuilds
  2. Nested ReactiveBuilder widgets inside keep only rebuild when their own state changes
  3. Use keep for:
    • Expensive widgets that don't depend on the current state
    • Other ReactiveBuilders that should update independently
    • Widgets with their own state management
    • Widgets that access the current state but don't need to rebuild when it changes

Note that there's no need to use keep on const widgets as they're already optimized by Flutter.

Debugging and Monitoring #

ReactiveNotifier provides extensive debugging information:

Creation and Lifecycle Tracking #

๐Ÿ”ง ViewModel<UserState> created
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
ID: instance_123
Location: package:my_app/user/user_viewmodel.dart:25
Initial state hash: 42
copied to clipboard

State Updates #

๐Ÿ“ ViewModel<UserState> updated
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
ID: instance_123
Update #: 1
New state hash: 84
copied to clipboard

Circular Reference Detection #

โš ๏ธ Invalid Reference Structure Detected!
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
Current Notifier: CartState
Key: cart_key
Problem: Attempting to create a circular dependency
Solution: Ensure state relationships form a DAG
Location: package:my_app/cart/cart_state.dart:42
copied to clipboard

Best Practices #

Organizing with Mixins #

Always organize ReactiveNotifier instances using mixins, not global variables:

// GOOD: Using mixins
mixin AuthService {
  static final ReactiveNotifier<UserState> userState = 
      ReactiveNotifier<UserState>(() => UserState.guest());
  
  static void login(String username, String password) {
    // Implementation
  }
}

// BAD: Don't use global variables
final userState = ReactiveNotifier<UserState>(() => UserState.guest());
copied to clipboard

Avoid Creating Instances in Widgets #

Never create ReactiveNotifier instances inside widgets:

// GOOD: Access through mixin
class ProfileWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReactiveBuilder(
      notifier: UserService.profileState,
      builder: (profile, keep) => Text(profile.name),
    );
  }
}

// BAD: Don't create instances in widgets
class BadProfileWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // DON'T DO THIS - creates a new instance on every build
    final profileState = ReactiveNotifier<Profile>(() => Profile());
    // ...
  }
}
copied to clipboard

State Cleaning in Widget Lifecycle #

Clean state appropriately when widgets are disposed:

class UserProfileScreen extends StatefulWidget {
  @override
  _UserProfileScreenState createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  @override
  void initState() {
    super.initState();
    // Optionally prepare initial state without notifications
    UserService.profileState.updateSilently(Profile.loading());
  }
  
  @override
  void dispose() {
    // Reset state but keep the instance
    UserService.resetProfile();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ReactiveBuilder(
      notifier: UserService.profileState,
      builder: (profile, keep) {
        // UI implementation
      },
    );
  }
}
copied to clipboard

Granular State Updates #

Use transformState with copyWith for efficient state updates:

mixin FormService {
  static final formState = ReactiveNotifier<FormState>(() => FormState.empty());
  
  static void updateEmail(String email) {
    formState.transformState((state) => state.copyWith(email: email));
  }
  
  static void updatePassword(String password) {
    formState.transformState((state) => state.copyWith(password: password));
  }
  
  // Use transformStateSilently for non-UI updates
  static void recordLastValidation() {
    formState.transformStateSilently((state) => 
      state.copyWith(lastValidated: DateTime.now()));
  }
}
copied to clipboard

Testing with ReactiveNotifier #

ReactiveNotifier makes testing straightforward by leveraging the original mixin and directly updating its state:

// Example test for CounterViewModel
void main() {
  group('CounterViewModel Tests', () {
    setUp(() {
      // Setup before each test
      ReactiveNotifier.cleanup(); // Clear previous states
      
      // Use the original mixin but update with mock data - recommended approach
      CounterService.viewModel.notifier.updateSilently(MockCounterState(
        count: 5,
        message: 'Test Initial State'
      ));
    });
    
    testWidgets('should display counter value', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: CounterWidget(), // Use the actual widget that uses CounterService
        ),
      );
      
      // Verify the widget displays the mocked state
      expect(find.text('Count: 5'), findsOneWidget);
      
      // Update state for the next test assertion
      CounterService.viewModel.notifier.increment();
      await tester.pump();
      
      // Verify widget updated correctly
      expect(find.text('Count: 6'), findsOneWidget);
    });
    
    test('service actions should update state correctly', () {
      // Set initial state for this specific test
      CounterService.viewModel.notifier.updateSilently(MockCounterState(
        count: 0,
        message: 'Fresh Test State'
      ));
      
      // Use the actual service methods
      CounterService.increment();
      
      // Verify state was updated correctly
      expect(CounterService.viewModel.state.count, equals(1));
      expect(CounterService.viewModel.state.message, contains('Incremented'));
    });
  });
}

// Mock model for testing
class MockCounterState extends CounterState {
  MockCounterState({required int count, required String message})
      : super(count: count, message: message);
}
copied to clipboard

This approach has several advantages:

  1. It uses the actual service mixin.
  2. No override.
  3. No container provider or similar.
  4. No force specific patter for testing, just natural :).
  5. It tests the real components with controlled data.
  6. It's simple and doesn't require complex mocking frameworks.
  7. It maintains the singleton pattern even during testing.

For specific test scenarios, you can easily reset or prepare different states:

// Before a specific test case
test('specific scenario', () {
  // Prepare specific test state
  CounterService.viewModel.notifier.updateSilently(MockCounterState(
    count: 100,
    message: 'Specific test scenario'
  ));
  
  // Run test logic...
});
copied to clipboard

Testing with ReactiveNotifier follows its core philosophy: use the same instances but control their state directly.

ReactiveNotifier is designed to work optimally with modular applications, following a feature-based MVVM architecture. We recommend the following project structure, although you can use the names and architecture that suit your needs. This example aims to give a clearer idea of the power of this library.

src/
โ”œโ”€โ”€ auth/                   # A specific feature
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ”‚   โ”œโ”€โ”€ views/
โ”‚   โ”‚   โ”œโ”€โ”€ widgets/
โ”‚   โ”‚   โ””โ”€โ”€ screens/
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ”œโ”€โ”€ user_model.dart
โ”‚   โ”‚   โ””โ”€โ”€ credentials_model.dart
โ”‚   โ”œโ”€โ”€ viewmodel/
โ”‚   โ”‚   โ””โ”€โ”€ auth_viewmodel.dart
โ”‚   โ”œโ”€โ”€ repository/         # Only if required
โ”‚   โ”‚   โ””โ”€โ”€ auth_repository.dart
โ”‚   โ”œโ”€โ”€ routes/             # Only if required
โ”‚   โ”‚   โ””โ”€โ”€ auth_routes.dart
โ”‚   โ””โ”€โ”€ notifier/
โ”‚       โ””โ”€โ”€ auth_notifier.dart (mixin)
โ”‚
โ”œโ”€โ”€ dashboard/              # Another specific feature
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ”‚   โ”œโ”€โ”€ views/
โ”‚   โ”‚   โ”œโ”€โ”€ widgets/
โ”‚   โ”‚   โ””โ”€โ”€ screens/
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ””โ”€โ”€ dashboard_model.dart
โ”‚   โ”œโ”€โ”€ viewmodel/
โ”‚   โ”‚   โ””โ”€โ”€ dashboard_viewmodel.dart
โ”‚   โ”œโ”€โ”€ repository/
โ”‚   โ”‚   โ””โ”€โ”€ dashboard_repository.dart
โ”‚   โ””โ”€โ”€ notifier/
โ”‚       โ””โ”€โ”€ dashboard_notifier.dart (mixin)
โ”‚
โ”œโ”€โ”€ profile/                # Another specific feature
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”œโ”€โ”€ viewmodel/
โ”‚   โ”œโ”€โ”€ repository/
โ”‚   โ””โ”€โ”€ notifier/
โ”‚
โ””โ”€โ”€ core/                   # Shared core components
    โ”œโ”€โ”€ api/
    โ”œโ”€โ”€ theme/
    โ””โ”€โ”€ utils/
copied to clipboard

Each feature follows the complete MVVM pattern with its own internal structure. This modular feature-based architecture allows for independent development and keeps related functionality grouped together.

Complete Architecture Example #

Below is an example of how to implement a complete system with this architecture:

1. Model

// src/auth/model/user_model.dart
class UserModel {
  final String id;
  final String name;
  final String email;
  final List<String> roles;
  
  const UserModel({
    required this.id,
    required this.name,
    required this.email,
    required this.roles,
  });
  
  UserModel copyWith({
    String? id,
    String? name,
    String? email,
    List<String>? roles,
  }) {
    return UserModel(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
      roles: roles ?? this.roles,
    );
  }
  
  factory UserModel.empty() => const UserModel(
    id: '',
    name: '',
    email: '',
    roles: [],
  );
}
copied to clipboard

2. Repository

// src/auth/repository/auth_repository.dart
import 'package:http/http.dart' as http;
import '../model/user_model.dart';

class AuthRepositoryImpl {
  AuthRepositoryImpl();
  final http.Client _client = http.Client();
  
  Future<UserModel> login(String email, String password) async {
    try {
      final response = await _client.post(
        Uri.parse('https://api.example.com/login'),
        body: {'email': email, 'password': password},
      );
      
      if (response.statusCode == 200) {
        // Parse response and return model
        return UserModel(
          id: '123',
          name: 'John Doe',
          email: email,
          roles: ['user'],
        );
      } else {
        throw Exception('Authentication failed');
      }
    } catch (e) {
      throw Exception('Login error: $e');
    }
  }
  
  Future<void> logout() async {
    await _client.post(Uri.parse('https://api.example.com/logout'));
  }
}
copied to clipboard

3. ViewModel

// src/auth/viewmodel/auth_viewmodel.dart
import 'package:reactive_notifier/reactive_notifier.dart';
import '../model/user_model.dart';
import '../repository/auth_repository.dart';
import '../../dashboard/notifier/dashboard_notifier.dart';
import '../../profile/notifier/profile_notifier.dart';

class AuthViewModel extends ViewModel<UserModel> {
  final AuthRepositoryImpl repository;

  // Pass repository by reference for better testability
  AuthViewModel(this.repository) : super(UserModel.empty());

  @override
  void init() {
    // Check for saved session
    checkSavedSession();
  }
  

  Future<void> checkSavedSession() async {
    // Logic to check for saved session
  }

  Future<void> login(String email, String password) async {
    try {
      // Show loading state
      transformState((state) => state.copyWith(
        name: 'Loading...',
      ));

      // Use injected repository for login
      final user = await repository.login(email, password);

      // Update state with user
      updateState(user);

      // CROSS-MODULE COMMUNICATION
      // Update other modules after successful login
      DashboardNotifier.dashboardState.updateSilently(DashboardModel.forUser(user));
      ProfileNotifier.profileState.updateSilently(ProfileModel.fromUser(user));

    } catch (e) {
      // Handle error and update state
      transformState((state) => UserModel.empty().copyWith(
          name: 'Error: ${e.toString()}'
      ));
    }
  }

  Future<void> logout() async {
    try {
      await repository.logout();

      // Clear state 
      cleanState();

      // CROSS-MODULE COMMUNICATION
      // Clear data from other modules
      DashboardNotifier.dashboardState.cleanCurrentNotifier();
      ProfileNotifier.profileState.cleanCurrentNotifier();

    } catch (e) {
      print('Logout error: $e');
    }
  }
}
copied to clipboard

4. Notifier (Mixin)

// src/auth/notifier/auth_notifier.dart
import 'package:reactive_notifier/reactive_notifier.dart';
import '../viewmodel/auth_viewmodel.dart';
import '../model/user_model.dart';

mixin AuthNotifier {
  // Main state
  static final authState = ReactiveNotifier<AuthViewModel>(() {
      final repository = AuthRepositoryImpl();
      return AuthViewModel(repository);
    },
  );
  
  // Utility methods for use in UI
  static Future<void> login(String email, String password) async {
    await authState.notifier.login(email, password);
  }
  
  static Future<void> logout() async {
    await authState.notifier.logout();
  }
  
  static bool get isLoggedIn {
    return authState.notifier.data.id.isNotEmpty;
  }
  
  static String get userName {
    return authState.notifier.data.name;
  }
  
  // More utility methods...
}
copied to clipboard

5. UI (Screen)

// src/auth/ui/screens/login_screen.dart
import 'package:flutter/material.dart';
import 'package:reactive_notifier/reactive_notifier.dart';
import '../../notifier/auth_notifier.dart';
import '../../model/user_model.dart';

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();


  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: ReactiveViewModelBuilder<UserModel>(
        viewmodel: AuthNotifier.authState.notifier,
        builder: (user, keep) {
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                if (user.name.startsWith('Error'))
                  Text(
                    user.name,
                    style: TextStyle(color: Colors.red),
                  ),
                
                // Clean UI with reusable widgets
                keep(LoginForm(
                  emailController: _emailController,
                  passwordController: _passwordController,
                  onLogin: ()async => await AuthNotifier.login(
                    _emailController.text,
                    _passwordController.text,
                  ),
                )),
              ],
            ),
          );
        },
      ),
    );
  }
  
}

// Reusable widget
class LoginForm extends StatelessWidget {
  final TextEditingController emailController;
  final TextEditingController passwordController;
  final VoidCallback onLogin;
  
  const LoginForm({
    required this.emailController,
    required this.passwordController,
    required this.onLogin,
  });
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: emailController,
          decoration: InputDecoration(labelText: 'Email'),
        ),
        TextField(
          controller: passwordController,
          decoration: InputDecoration(labelText: 'Password'),
          obscureText: true,
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: onLogin,
          child: Text('Login'),
        ),
      ],
    );
  }
}
copied to clipboard

Cross-Module Communication #

ReactiveNotifier facilitates direct communication between modules without complex event systems. All logic can occur within the ViewModel, Notifier, or even the Repository.

Communication through ViewModel #

// In auth_viewmodel.dart
Future<void> login(String email, String password) async {
  try {
    // ... login code
    
    // After successful login, update other modules
    // Note we're not using 'related', but direct communication
    
    // Update dashboard with user data
    DashboardNotifier.dashboardState.updateState(
      DashboardModel.forUser(user)
    );
    
    // Prepare profile data silently (without rebuilding UI yet)
    ProfileNotifier.profileState.updateSilently(
      ProfileModel.fromUser(user)
    );
    
    // Cart might require additional data.
    CartNotifier.cartState.updateStateFromUserId(user.id);
    
  } catch (e) {
    // Error handling
  }
}
copied to clipboard

Communication through Notifier (Mixin) #

// In payment_notifier.dart
mixin PaymentNotifier {
  static final paymentState = ReactiveNotifier<PaymentViewModel>(
    () => PaymentViewModel(),
  );
  
  static Future<void> processPayment(String orderId) async {
    try {
      final result = await paymentState.notifier.processPayment(orderId);
      
      if (result.success) {
        // Update other modules after successful payment
        OrderNotifier.orderState.notifier.updateOrderStatus(
          orderId, 
          'PAID'
        );
        
        CartNotifier.cartState.notifier.clearCart();
        
        NotificationNotifier.notificationState.notifier.addNotification(
          NotificationModel(
            title: 'Payment Successful',
            body: 'Your order #$orderId has been paid.',
            type: NotificationType.success,
          ),
        );
      }
    } catch (e) {
      // Error handling
    }
  }
}
copied to clipboard

Communication from Repository #

// In order_repository.dart
class OrderRepositoryImpl {
  // ... other methods
  
  Future<OrderModel> createOrder(CartModel cart) async {
    try {
      // API call to create order
      final orderResponse = await _client.post(...);
      final newOrder = OrderModel.fromJson(orderResponse);
      
      // Update related modules directly from repository
      CartNotifier.cartState.notifier.setOrderId(newOrder.id);
      
      // We can even trigger navigation if needed
      NavigationNotifier.navigate('order_confirmation', params: {'id': newOrder.id});
      
      return newOrder;
    } catch (e) {
      throw Exception('Failed to create order: $e');
    }
  }
}
copied to clipboard

The main advantage of this approach is that it keeps business logic and cross-module communication in the appropriate layers (ViewModel, Repository), leaving the UI clean and focused solely on presentation. No complex event systems, or additional controllers are required.

Examples #

Check out our example app for more comprehensive examples and use cases.

Contributing #

  1. Fork it
  2. Create your feature branch (git checkout -b feature/amazing)
  3. Commit your changes (git commit -am 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing)
  5. Create a new Pull Request

Support #

  • ๐ŸŒŸ Star the repo to show support
  • ๐Ÿ› Create an issue for bugs
  • ๐Ÿ’ก Submit feature requests through issues
  • ๐Ÿ“ Contribute to the documentation

License #

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


Made with โค๏ธ by JhonaCode

4
likes
160
points
569
downloads

Publisher

verified publisherjhonacode.com

Weekly Downloads

2024.09.13 - 2025.03.28

A Dart library for managing reactive state efficiently, supporting multiples related state.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on reactive_notifier