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.
Note: Are you migrating from
reactive_notify
? The API remains unchanged - just update your dependency toreactive_notifier
.
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
Installation
Add this to your package's pubspec.yaml
file:
dependencies:
reactive_notifier: ^2.4.2
Quick Start
Usage with ReactiveNotifier
Learn how to implement ReactiveNotifier across different use cases - from basic data types (String
, bool
) to complex classes (ViewModels
). These examples showcase global state management patterns that maintain accessibility across your application.
With Classes, Viewmodel, etc.
/// It is recommended to use mixin to save your notifiers, from a static variable.
///
mixin ConnectionService{
static final ReactiveNotifier<ConnectionManager> instance = ReactiveNotifier<ConnectionManager>(() => ConnectionManager());
}
ReactiveBuilder<ConnectionManager>(
notifier: ConnectionService.instance,
builder: ( service, keep) {
/// Notifier is used to access your model's data.
final state = service.notifier;
return Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
CircleAvatar(
radius: 30,
backgroundColor: state.color.withValues(alpha: 255 * 0.2),
child: Icon(
state.icon,
color: state.color,
size: 35,
),
),
Text(
state.message,
style: Theme.of(context).textTheme.titleMedium,
),
if (state.isError || state == ConnectionState.disconnected)
keep(
ElevatedButton.icon(
/// If you don't use notifier, access the functions of your Viewmodel that contains your model.
onPressed: () => service.manualReconnect(),
icon: const Icon(Icons.refresh),
label: const Text('Retry Connection'),
),
),
if (state.isSyncing) const LinearProgressIndicator(),
],
),
),
);
},
);
With simple values.
mixin ConnectionService{
static final ReactiveNotifier<String> instance = ReactiveNotifier<String>(() => "N/A");
}
// Declare a simple state
ReactiveBuilder<ConnectionManager>(
notifier: ConnectionService.instance,
builder: ( state, keep) => Text(state),
);
/// Modify from other widget.
class OtherWidget extends StatelessWidget {
const OtherWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
OutlinedButton(onPressed: (){
ConnectionService.instance.updateState("New value");
}, child: Text('Edit String'))
],
);
}
}
/// Modify from any class, etc.
class OtherViewModel{
void otherFunction(){
///Come code.....
ConnectionService.instance.updateState("Value from other viewmodel");
}
}
/// This example above applies to any Reactive Notifier, no matter how complex, it can be used from anywhere without the need for a reference or handler.
Both simple and complex values can be modified from anywhere in the application without modifying the structure of your widget. You also don't need to instantiate variables in your widget build, you just call the mixin directly where you want to use it, this helps with less coupling, being able to replace all functions from the mixin and not fight with extensive migrations.
ViewModelStateImpl
without Repository
For simpler cases where direct state management is needed without a repository layer, you can use ViewModelStateImpl
. This approach manages state directly within the ViewModel:
class CartViewModel extends ViewModelStateImpl<CartModel> {
CartViewModel() : super(CartModel());
// Add product directly to state
void addProduct(String item, double price) {
final currentItems = notifier.items;
final newItems = [...currentItems, item];
final newTotal = notifier.total + price;
// Or using transformState
transformState((state) => state.copyWith(items: newItems, total: newTotal));
// Or dreate new instance and replace.
updaeState(newCarInstance);
}
// Other business logic methods...
}
This implementation is suitable for:
- When you don't require repository at ViewModel level
- When implementing different architectural patterns (MVP, DAO, Clean Architecture)
- Any complexity level of state management
- Freedom to implement data persistence and business logic as needed
- Flexibility to structure your code without being tied to specific architectural constraints
Using the Library Repository with ViewModelImpl
In this example, we are going to use the library's built-in repository to get and update the shopping cart data in the ViewModelImpl
.
1. Defining the Repository using the library
First, instead of creating a repository manually, we are going to use a repository provided by the library to interact with the data. Let's say you have a repository to handle cart-related data.
import 'package:reactive_notifier/reactive_notifier.dart';
class CartRepository extends RepositoryImpl<CartModel> {
// We simulate the loading of a shopping cart
Future<CartModel> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return CartModel(
items: ['Product A', 'Product B'],
total: 39.98,
);
}
/// More functions .....
}
2. ViewModelImpl
with the Repository
Now we are going to use the repository in the ViewModelImpl
to interact with the cart model. The ViewModelImpl
will leverage the repository to get data and make updates.
class CartViewModel extends ViewModelImpl<CartModel> {
final CartRepository repository;
CartViewModel(this.repository) : super(CartModel());
// Function to load the cart from the repository
Future<void> loadShoppingCart() async {
try {
// We get the cart from the repository
final shoppingCart = await repository.fetchData();
updateState(carrito); // We update the status with the cart loaded
} catch (e) {
// Error handling
print("Error: $e");
}
}
// Function to add a product to the cart
Future<void> addProduct(String item, double price) async {
try {
await repository.addProduct(value, item, price);
// We update the status after adding the product
updateState(notifier.copyWith(items: value.items, total: value.total));
// Or
transformState((state) => state.copyWith(items: value.items, total: value.total));
// Or
updateState(yourModelWithData);
} catch (e) {
// Error handling
print("Error $e");
}
}
}
3. Repository Instance and ViewModelImpl
Here we create the repository instance and the ViewModelImpl
:
final cartViewModel = ReactiveNotifier<CartViewModel>((){
final cartRepository = CartRepository();
return CartViewModel(cartRepository);
});
4. Cart Status Widget
Finally, we are going to display the cart status in the UI using ReactiveBuilder
, which will automatically update when the status changes.
ReactiveBuilder<CartViewModel>(
notifier: cartViewModel,
builder: ( viewModel, keep) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (viewModel.data.isEmpty)
keep(Text("Loading cart...")),
if (viewModel.data.isNotEmpty) ...[
keep(Text("Products in cart:")),
...viewModel.data.map((item) => Text(item)).toList(),
keep(const SizedBox(height: 20)),
Text("Total: \$${viewModel.total.toStringAsFixed(2)}"),
keep(const SizedBox(height: 20)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
keep(
ElevatedButton(
onPressed: () {
// Add a new product
cartViewModel.notifier.agregarProducto("Producto C", 29.99);
},
child: Text("Agregar Producto C"),
),
),
keep(const SizedBox(width: 10)),
keep(
ElevatedButton(
onPressed: () {
// Empty cart
cartViewModel.notifier.myCleaningCarFunction();
},
child: Text("Vaciar Carrito"),
),
),
],
),
],
],
);
},
)
β‘οΈ Essential: ReactiveNotifier
Core Concepts
Documentation for related
in ReactiveNotifier
The related
attribute in ReactiveNotifier
allows you to efficiently manage interdependent states. They can be used in different ways depending on the structure and complexity of the state you need to handle.
Using related
in ReactiveNotifier
Establishing Relationships Between Notifiers
ReactiveNotifier
can manage any type of data (simple or complex). Through therelated
property, you can establish connections between different notifiers, where changes in any related notifier will trigger updates in theReactiveBuilder
that's watching them.
Example Scenario: Managing Connected States
- A primary notifier might handle a complex
UserInfo
class, while other notifiers manage related states likeSettings
orPreferences
. Usingrelated
, any changes in these interconnected states will trigger the appropriate UI updates throughReactiveBuilder
.
Direct Relationship between Simple Notifiers
In this approach, you have several simple ReactiveNotifier
s, and you use them together to notify state changes when any of these notifiers changes. The ReactiveNotifier
s are related to each other using the related
attribute, and you see a combined ReactiveBuilder
.
Example
final timeHoursNotifier = ReactiveNotifier<int>(() => 0);
final routeNotifier = ReactiveNotifier<String>(() => '');
final statusNotifier = ReactiveNotifier<bool>(() => false);
// A combined ReactiveNotifier that watches for changes in all three notifiers
final combinedNotifier = ReactiveNotifier(
() {},
related: [timeHoursNotifier, routeNotifier, statusNotifier],
);
- Explanation:
Here,
combinedNotifier
is aReactiveNotifier
that updates when any of the three notifiers (timeHoursNotifier
,routeNotifier
,statusNotifier
) changes. This is useful when you have several simple states and you want them all to be connected to trigger an update in the UI together.
ReactiveBuilder(
notifier: combinedNotifier,
builder: (state, keep) {
return Column(
children: [
Text("Hours: ${timeHoursNotifier.value}"),
Text("Route: ${routeNotifier.value}"),
Text("State: ${statusNotifier.value ? 'Active' : 'Inactive'}"),
],
);
},
);
- Explanation:
ReactiveBuilder
watches thecombinedNotifier
. Since the related notifiers are configured, any changes totimeHoursNotifier
,routeNotifier
, orstatusNotifier
will automatically update the UI.
** Relationship between a Main ReactiveNotifier
and Other Complementary Notifiers**
In this approach, you have a main ReactiveNotifier
that handles a more complex class, such as a UserInfo
object, and other complementary ReactiveNotifier
s are related through related
. These complementary notifiers do not need to be declared inside the main object class, but are integrated with it through the related
attribute.
Example: UserInfo
with Settings
Imagine that we have a UserInfo
class that represents a user's information, and a Settings
class that contains complementary settings. The notifiers for these states are related to each other so that any change in Settings
or UserInfo
triggers a global update.
class UserInfo {
final String name;
final int age;
UserInfo({required this.name, required this.age});
// Constructor for default values
UserInfo.empty() : name = '', age = 0;
// Method to clone with new values
UserInfo copyWith({String? name, int? age}) {
return UserInfo(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// Complementary notifiers for configurations
final settingsNotifier = ReactiveNotifier<String>(() => 'Dark Mode');
final notificationsEnabledNotifier = ReactiveNotifier<bool>(() => true);
// Combined ReactiveNotifier that watches all related notifiers
final userStateNotifier = ReactiveNotifier<UserInfo>(
() => UserInfo.empty(),
related: [settingsNotifier, notificationsEnabledNotifier],
);
- Explanation:
In this example,
userStateNotifier
is the mainReactiveNotifier
that handles the state ofUserInfo
.settingsNotifier
andnotificationsEnabledNotifier
are companion notifiers that handle user settings such as dark mode and enabling notifications. While they are not declared withinUserInfo
, they are related to it viarelated
.
ReactiveBuilder<UserInfo>(
notifier: userStateNotifier,
builder: ( userInfo, keep) {
return Column(
children: [
Text("User: ${userInfo.name}, Age: ${userInfo.age}"),
Text("Configuration: ${settingsNotifier.notifier}"),
Text("Notifications: ${notificationsEnabledNotifier.notifier ? 'Active' : 'Inactive'}"),
],
);
},
);
- Explanation:
ReactiveBuilder
watchesuserStateNotifier.value
(the user state). It also watches the related notifiers (settings and notifications). This means that any change to any of these notifiers will trigger an update in the UI.
Usage with ReactiveBuilder.notifier
ReactiveBuilder(
notifier: userStateNotifier,
builder: (userInfo, keep) {
return Column(
children: [
ElevatedButton(
onPressed: () {
userStateNotifier.updateState(userInfo.copyWith(name: "Nuevo Nombre"));
},
child: Text("Actualizar Nombre"),
),
],
);
},
);
Advantages of Using related
in ReactiveNotifier
-
Flexibility: You can relate simple and complex notifiers without the need to involve additional classes. This is useful for handling states that depend on multiple values without overcomplicating the structure.
-
Optimization: When related notifiers change, the UI is automatically updated without the need to manually manage dependencies. This streamlines the workflow and improves the performance of the application.
-
Scalability: As your application grows, you can easily add more notifiers and relate them without modifying the existing logic, simply by extending the list of
related
. -
Simplicity: You can easily handle complex states using
related
, keeping everything decoupled and clean without the need to wrap everything in a single ViewModel.
Accessing Related States within a ReactiveBuilder
When you have multiple related ReactiveNotifier
s, you can access the states in a number of ways within a ReactiveBuilder
. Here I will explain the different ways to do this:
- Directly accessing the related
ReactiveNotifier
s. - Using the
from<T>()
method to access the related states within aReactiveNotifier
. - Using
keyNotifier
to access a specificReactiveNotifier
.
General Example: Relating Notifiers and Accessing their States
First, we will define the individual notifiers and then create a relationship between them using the related
attribute within a parent ReactiveNotifier
.
Defining Notifiers and Relationships
final userState = ReactiveNotifier<UserState>(() => UserState());
final cartState = ReactiveNotifier<CartState>(() => CartState());
final settingsState = ReactiveNotifier<SettingsState>(() => SettingsState());
final appState = ReactiveNotifier<AppState>(
() => AppState(),
related: [userState, cartState, settingsState],
);
userState
,cartState
andsettingsState
are individual states, andappState
is the mainReactiveNotifier
that is related to them. This means that when any of the related states change,appState
will be automatically updated.
1. Accessing Related States Directly
In a ReactiveBuilder
, you can directly access related notifiers without using additional methods like from<T>()
or keyNotifier
. You simply use the notifiers directly inside the builder
.
Usage in Direct ReactiveBuilder
class AppDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveBuilder<AppState>(
notifier: appState,
builder: (state, keep) {
final user = userState.notifier.data;
final cart = cartState.notifier.data;
final settings = settingsState.notifier.data;
return Column(
children: [
Text('Welcome ${user.name}'),
Text('Cart Items: ${cart.items.length}'),
Text('Settings: ${settings.theme}'),
if (user.isLoggedIn) keep(const UserProfile())
],
);
},
);
}
}
- Explanation:
- Here, we directly access the values of
userState
,cartState
, andsettingsState
using.notifier.data
. - Pros: It's a quick and straightforward way to access the values if you don't need to perform any extra logic on them.
- Cons: If you need to access a specific value of a related
ReactiveNotifier
and it's not directly in thebuilder
, you might need something more organized, like usingkeyNotifier
or thefrom<T>()
method.
2. Using the from<T>()
Method
The from<T>()
method is used to access a related state within a ReactiveNotifier
. This method allows you to access a specific state more explicitly, especially if you need to get the value of a related state without directly accessing the ReactiveNotifier
.
Usage with from<T>()
class AppDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveBuilder<AppState>(
notifier: appState,
builder: (state, keep) {
final user = appState.from<UserState>();
final cart = appState.from<CartState>();
final settings = appState.from<SettingsState>();
return Column(
children: [
Text('Welcome ${user.name}'),
Text('Cart Items: ${cart.items.length}'),
Text('Settings: ${settings.theme}'),
if (user.isLoggedIn) keep(const UserProfile())
],
);
},
);
}
}
- Explanation:
- We use
appState.from<UserState>()
to access the related user state. Similarly, we usecartState.keyNotifier
to accessCartState
using itskeyNotifier
. - Pros:
from<T>()
is useful when you have multiple related states and want to extract a value from a specific one more explicitly. - Cons: Although it is more organized, it can add complexity if you only need to access one or two states in a simple way.
3. Using keyNotifier
to Access Specific Notifiers
The keyNotifier
is useful when you want to access a related state that has a unique key within the related
relationship. This is especially useful when you have multiple notifiers of the same type (for example, multiple cartState's
) and you need to distinguish between them.
Using keyNotifier
class AppDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveBuilder<AppState>(
notifier: appState,
builder: (state, keep) {
final user = appState.from<UserState>(userState.keyNotifier); /// Or appState.from(userState.keyNotifier)
final cart = appState.from<CartState>(cartState.keyNotifier); /// ....
final settings = appState.from<SettingsState>(settingsState.keyNotifier); /// ....
return Column(
children: [
Text('Welcome ${user.name}'),
Text('Cart Items: ${cart.items.length}'),
Text('Settings: ${settings.theme}'),
if (user.isLoggedIn) keep(const UserProfile())
],
);
},
);
}
}
- Explanation:
appState.from<CartState>(cartState.keyNotifier)
accesses the cart state using itskeyNotifier
.- Pros: Using
keyNotifier
is useful when you have states of similar types or when you want to specify which instance of aReactiveNotifier
to use within a relationship. - Cons: If you only have one
ReactiveNotifier
of each type, this may be unnecessary, but in more complex scenarios with multiple notifiers of the same type, it's a great way to distinguish them.
What to Avoid
// β NEVER: Nested related states
final cartState = ReactiveNotifier<CartState>(
() => CartState(),
related: [userState] // β Don't do this
);
// β NEVER: Chain of related states
final orderState = ReactiveNotifier<OrderState>(
() => OrderState(),
related: [cartState] // β Avoid relation chains
);
// β
CORRECT: Flat structure with single parent
final appState = ReactiveNotifier<AppState>(
() => AppState(),
related: [userState, cartState, orderState]
);
Async & Stream Support
Async Operations
class ProductViewModel extends AsyncViewModelImpl<List<Product>> {
@override
Future<List<Product>> fetchData() async {
return await repository.getProducts();
}
}
class ProductsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveAsyncBuilder<List<Product>>(
notifier: productViewModel,
onSuccess: (products) => ProductGrid(products),
onLoading: () => const LoadingSpinner(),
onError: (error, stack) => ErrorWidget(error),
onInitial: () => const InitialView(),
);
}
}
Stream Handling
final messagesStream = ReactiveNotifier<Stream<Message>>(
() => messageRepository.getMessageStream()
);
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveStreamBuilder<Message>(
notifier: messagesStream,
onData: (message) => MessageBubble(message),
onLoading: () => const LoadingIndicator(),
onError: (error) => ErrorMessage(error),
onEmpty: () => const NoMessages(),
onDone: () => const StreamComplete(),
);
}
}
Debugging System
ReactiveNotifier includes a comprehensive debugging system with detailed error messages:
Creation Tracking
π¦ Creating ReactiveNotifier<UserState>
π With related types: CartState, OrderState
Invalid Structure Detection
β οΈ Invalid Reference Structure Detected!
ββββββββββββββββββββββββββββββ
Current Notifier: CartState
Key: cart_key
Problem: Attempting to create a notifier with an existing key
Solution: Ensure unique keys for each notifier
Location: package:my_app/cart/cart_state.dart:42
Performance Monitoring
β οΈ Notification Overflow Detected!
ββββββββββββββββββββββββββββββ
Notifier: CartState
50 notifications in 500ms
β Problem: Excessive updates detected
β
Solution: Review update logic and consider debouncing
And more...
Best Practices
State Declaration
- Declare ReactiveNotifier instances globally or as static mixin members
- Never create instances inside widgets
- Use mixins for better organization of related states
Performance Optimization
- Use
keep
for static content - Maintain flat state hierarchy
- Use keyNotifier for specific state access
- Avoid unnecessary rebuilds
Architecture Guidelines
- Follow MVVM pattern
- Utilize Repository/Service patterns
- Let ViewModels initialize automatically
- Keep state updates context-independent
Related States
- Maintain flat relationships
- Avoid circular dependencies
- Use type-safe access
- Keep state updates predictable
Coming Soon: Real-Time State Inspector π
We're developing a powerful visual debugging interface that will revolutionize how you debug and monitor ReactiveNotifier states:
Features in Development
- π Real-time state visualization
- π Live update tracking
- π Performance metrics
- πΈοΈ Interactive dependency graph
- β±οΈ Update timeline
- π Deep state inspection
- π± DevTools integration
This tool will help you:
- Understand state flow in real-time
- Identify performance bottlenecks
- Debug complex state relationships
- Monitor rebuild patterns
- Optimize your application
- Develop more efficiently
Examples
Check out our example app for more comprehensive examples and use cases.
Contributing
We love contributions! Please read our Contributing Guide first.
- Fork it
- Create your feature branch (
git checkout -b feature/amazing
) - Commit your changes (
git commit -am 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing
) - 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
Libraries
- reactive_notifier
- A library for managing reactive state in Flutter applications.