🏛️

ReactiveController

class

Core controller class for managing reactive state in your Flutter applications

📅 Since v1.0.0📦 Core API

Signature

class ReactiveController extends ChangeNotifier {
  ReactiveController();
  
  @protected
  T state<T>(T initialValue);
  
  @protected
  void setState(VoidCallback fn);
  
  @override
  void dispose();
}

Examples

ReactiveController

The ReactiveController class is the foundation of reactiv's reactive state management system. It provides a simple and intuitive API for managing application state with automatic dependency tracking and efficient re-rendering.

Overview

ReactiveController extends Flutter's ChangeNotifier and provides reactive state management capabilities. When you modify reactive state within a controller, all dependent widgets automatically rebuild.

Signature

class ReactiveController extends ChangeNotifier {
  ReactiveController();
  
  @protected
  T state<T>(T initialValue);
  
  @protected
  void setState(VoidCallback fn);
  
  @override
  void dispose();
}

Constructor

ReactiveController()

Creates a new instance of ReactiveController.

Parameters: None

Example:

class TodoController extends ReactiveController {
  TodoController() {
    _todos = state<List<Todo>>([]);
    _filter = state<TodoFilter>(TodoFilter.all);
  }
  
  late final Reactive<List<Todo>> _todos;
  late final Reactive<TodoFilter> _filter;
}

Methods

state<T>(T initialValue)

Creates a reactive state variable with the given initial value. The state is automatically tracked, and widgets that read this state will rebuild when it changes.

Type Parameters:

  • T - The type of the state value

Parameters:

  • initialValue (T, required) - The initial value for the state

Returns: Reactive<T> - A reactive wrapper around the state value

Example:

class CounterController extends ReactiveController {
  CounterController() {
    _count = state<int>(0);
  }
  
  late final Reactive<int> _count;
  
  int get count => _count.value;
  
  void increment() {
    _count.value++;
  }
}

See Also:

setState(VoidCallback fn)

Executes the given callback and notifies listeners of state changes. This is useful when you need to perform multiple state updates as a single transaction.

Parameters:

  • fn (VoidCallback, required) - Callback function containing state updates

Returns: void

Example:

void addMultipleTodos(List<String> titles) {
  setState(() {
    for (final title in titles) {
      _todos.value = [..._todos.value, Todo(title: title)];
    }
  });
}

Note: While setState is available, in most cases you can simply update reactive state directly and changes will be automatically tracked.

dispose()

Disposes of the controller and cleans up resources. This method is automatically called when the controller is no longer needed.

Returns: void

Example:

@override
void dispose() {
  // Clean up any subscriptions or resources
  _subscription?.cancel();
  super.dispose();
}

Important: Always call super.dispose() when overriding this method.

Properties

value

Access the current value of a reactive state. Setting this property automatically triggers a rebuild of dependent widgets.

Type: T (generic type of the state)

Example:

// Reading state
final currentCount = _count.value;

// Updating state
_count.value = currentCount + 1;

// Or directly
_count.value++;

Usage Examples

Basic Counter

class CounterController extends ReactiveController {
  CounterController() {
    _count = state<int>(0);
  }
  
  late final Reactive<int> _count;
  
  int get count => _count.value;
  
  void increment() => _count.value++;
  void decrement() => _count.value--;
  void reset() => _count.value = 0;
}

// In your widget
class CounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = use.controller(() => CounterController());
    
    return Scaffold(
      body: Center(
        child: Text('Count: ${controller.count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Todo List with Filtering

enum TodoFilter { all, active, completed }

class TodoController extends ReactiveController {
  TodoController() {
    _todos = state<List<Todo>>([]);
    _filter = state<TodoFilter>(TodoFilter.all);
  }
  
  late final Reactive<List<Todo>> _todos;
  late final Reactive<TodoFilter> _filter;
  
  List<Todo> get filteredTodos {
    switch (_filter.value) {
      case TodoFilter.all:
        return _todos.value;
      case TodoFilter.active:
        return _todos.value.where((t) => !t.completed).toList();
      case TodoFilter.completed:
        return _todos.value.where((t) => t.completed).toList();
    }
  }
  
  void addTodo(String title) {
    _todos.value = [
      ..._todos.value,
      Todo(
        id: DateTime.now().toString(),
        title: title,
        completed: false,
      ),
    ];
  }
  
  void toggleTodo(String id) {
    _todos.value = _todos.value.map((todo) {
      return todo.id == id
          ? todo.copyWith(completed: !todo.completed)
          : todo;
    }).toList();
  }
  
  void setFilter(TodoFilter filter) {
    _filter.value = filter;
  }
}

Async Data Fetching

class UserController extends ReactiveController {
  UserController(this._api) {
    _user = state<User?>(null);
    _loading = state<bool>(false);
    _error = state<String?>(null);
  }
  
  final ApiService _api;
  
  late final Reactive<User?> _user;
  late final Reactive<bool> _loading;
  late final Reactive<String?> _error;
  
  User? get user => _user.value;
  bool get loading => _loading.value;
  String? get error => _error.value;
  
  Future<void> fetchUser(String id) async {
    _loading.value = true;
    _error.value = null;
    
    try {
      final user = await _api.getUser(id);
      _user.value = user;
    } catch (e) {
      _error.value = e.toString();
    } finally {
      _loading.value = false;
    }
  }
  
  @override
  void dispose() {
    // Clean up if needed
    super.dispose();
  }
}

Best Practices

  1. Initialization: Always initialize reactive state in the constructor using state<T>(initialValue)

  2. Immutability: When updating complex objects like lists or maps, create new instances instead of mutating:

    // Good
    _todos.value = [..._todos.value, newTodo];
    
    // Bad
    _todos.value.add(newTodo); // Won't trigger updates!
    
  3. Computed Values: Use getters for derived state:

    List<Todo> get completedTodos => 
        _todos.value.where((t) => t.completed).toList();
    
  4. Cleanup: Override dispose() to clean up subscriptions and resources

  5. Single Responsibility: Keep controllers focused on a single feature or domain

Performance Considerations

  • Reactive state automatically tracks dependencies and only rebuilds affected widgets
  • Batch multiple updates together when possible to minimize rebuilds
  • Use const widgets where possible to prevent unnecessary rebuilds
  • Consider breaking large controllers into smaller, focused ones

Common Pitfalls

  1. Forgetting to use .value: Always access reactive state through the .value property

    // Wrong
    final count = _count;
    
    // Correct
    final count = _count.value;
    
  2. Mutating collections: Create new instances instead of mutating

  3. Not calling super.dispose(): Always call the parent's dispose method

Related APIs

See Also

Was this page helpful?

Help us improve our documentation by providing feedback.