Fluxivity
Fluxivity is an experimental package to study the effect of building a reactive graph.
Package is available on pub.dev here
Why this package
Despite the availability of a lot of state management solutions, I often find myself looking for simpler solutions that are bound by the following principles
- Global app state
- View Models should only contain value/data accessors and not the data themselves
- It should be possible to construct a graph like structure of App state.
- It should be possible to access latest data without waiting on a value
- It should be possible to track changes to data if required
- Have a functional approach to dealing with changes
Note: 2023-04-07: I recently saw an implementation with the package being used with traditional view models. While I am not a huge fan of encapsulating data in view models, I do see the value in this approach. I will be adding an example of this in the future.
Basic functionality
There are 2 core classes
- Reactive
- Computed
A reactive class creates a reactive property and keeps track of its state publishing updates as
the value changes through a stream. The latest value is always available as a static value
without the need to call await
or listen
. However there is a stream available that can be
listened to in case you want to listen for updates. The reactive class also contains a addEffect
method to handle side effects as part of the mutation of the value. For example, updating data in
a offline store, or syncing data to the backend.
There is an associated computed class that functions similar to a reactive class, however its value is always dependent on the classes it depends on for its value. The value of a computed class gets updated everytime the value of the dependent classes change. This can be used where derived values are necessary.
Example Usage
You can also refer to the test files for example usage. I will add additional example apps when I find more time
CurrentPage page = CurrentPage({location: 'test'});
Reactive<String> appId = Reactve('app_123456789');
Reactive<CurrentPage> currentPage = Reactive(page);
Computed<String> location = Computed([currentPage], (List<Reactive<CurrentPage>> cpl ) {
Reactive<CurrentPage> page = cpl.first();
return page.location
})
currentPage.value.location;
// test
location.value
// test
page = CurrentPage({location: 'newLocation'});
currentPage.value = page;
currentPage.stream.listen((Snapshot<CurrentPage> snapshot) {
snapshot.oldValue.location;
// test
snapshot.newValue.location;
// newLocation
})
currentPage.value.location;
// newLocation
location.value
// newLocation
Reactive Collections
Fluxivity provides reactive collection extensions for Lists, Maps, and Sets, which allow you to easily create and work with reactive collections in your Dart applications. With these extension methods, you can automatically track changes to your collections, and the package will handle the updates and emit a stream whenever the collection is manipulated.
Using Reactive Lists
To create a reactive list, use the reactive getter on a regular list:
import 'package:fluxivity/fluxivity.dart';
void main() {
final reactiveList = [1, 2, 3].reactive;
// You can subscribe to changes in the list
reactiveList.stream.listen((snapshot) {
print('List updated: ${snapshot.newValue}');
});
// Use the regular list methods to manipulate the list
reactiveList.value.add(4); // Output: List updated: [1, 2, 3, 4]
}
Using Reactive Maps
For key-value collections, you can use reactive maps:
import 'package:fluxivity/fluxivity.dart';
void main() {
// Create a reactive map
final userProfiles = <String, Map<String, dynamic>>{
'user1': {'name': 'Alice', 'age': 28},
}.reactive;
// Subscribe to changes
userProfiles.stream.listen((snapshot) {
print('Map updated: ${snapshot.newValue}');
});
// Modify the map
userProfiles.value['user2'] = {'name': 'Bob', 'age': 32};
// Output: Map updated: {user1: {name: Alice, age: 28}, user2: {name: Bob, age: 32}}
// Update an existing entry
final bobProfile = Map<String, dynamic>.from(userProfiles.value['user2']!);
bobProfile['age'] = 33;
userProfiles.value['user2'] = bobProfile;
// Output: Map updated: {user1: {name: Alice, age: 28}, user2: {name: Bob, age: 33}}
}
Using Reactive Sets
For collections of unique values, you can use reactive sets:
import 'package:fluxivity/fluxivity.dart';
void main() {
// Create a reactive set
final activeUsers = <String>{'user1', 'user2'}.reactive;
// Subscribe to changes
activeUsers.stream.listen((snapshot) {
print('Active users: ${snapshot.newValue}');
});
// Add a new unique value
activeUsers.value.add('user3');
// Output: Active users: {user1, user2, user3}
// Adding a duplicate has no effect
activeUsers.value.add('user2');
// No output (set already contains 'user2')
// Remove a value
activeUsers.value.remove('user1');
// Output: Active users: {user2, user3}
}
Set Operations with Computed
Reactive sets work especially well with Computed for set operations:
import 'package:fluxivity/fluxivity.dart';
void main() {
// Create two reactive sets
final adminUsers = <String>{'alice', 'bob'}.reactive;
final activeUsers = <String>{'alice', 'charlie', 'david'}.reactive;
// Create a computed value for users who are both admins and active
final activeAdmins = Computed<Set<String>>([adminUsers, activeUsers], (sources) {
final admins = sources[0].value as Set<String>;
final active = sources[1].value as Set<String>;
return admins.intersection(active);
});
print(activeAdmins.value); // Output: {alice}
// Update the admin set
adminUsers.value = {'alice', 'bob', 'charlie'};
print(activeAdmins.value); // Output: {alice, charlie}
}
Unwrap Method
All reactive collections provide an unwrap method to retrieve the original collection. This is helpful when working with external libraries or APIs:
final nonReactiveList = reactiveList.unwrap();
final nonReactiveMap = reactiveMap.unwrap();
final nonReactiveSet = reactiveSet.unwrap();
Adding Effects
All reactive collections support the addEffect method to define custom functions that will be executed on changes:
// For reactive lists
reactiveList.addEffect((snapshot) {
print('List changed from ${snapshot.oldValue} to ${snapshot.newValue}');
});
// For reactive maps
reactiveMap.addEffect((snapshot) {
print('Map changed with ${snapshot.newValue.length} entries');
});
// For reactive sets
reactiveSet.addEffect((snapshot) {
final added = snapshot.newValue.difference(snapshot.oldValue);
final removed = snapshot.oldValue.difference(snapshot.newValue);
print('Added: $added, Removed: $removed');
});
Collection Helper Methods and Extensions
Fluxivity's reactive collections can be enhanced with custom helper methods to streamline common operations while maintaining reactivity. Here are some examples you can implement:
List Helper Methods
// Add these extension methods to enhance ReactiveListHelpers
extension EnhancedReactiveListHelpers<E> on Reactive<List<E>> {
// Sort the list with a comparator and trigger reactivity
void sort([int Function(E a, E b)? compare]) {
final newList = List<E>.from(value);
newList.sort(compare);
value = newList;
}
// Filter the list and return a new reactive list
Reactive<List<E>> where(bool Function(E) test) {
return value.where(test).toList().reactive;
}
// Map the list to a new reactive list of different type
Reactive<List<T>> mapToReactive<T>(T Function(E) convert) {
return value.map(convert).toList().reactive;
}
}
Map Helper Methods
// Add these extension methods to enhance ReactiveMapHelpers
extension EnhancedReactiveMapHelpers<K, V> on Reactive<Map<K, V>> {
// Create a new map containing only entries that satisfy a test
Reactive<Map<K, V>> filter(bool Function(K key, V value) test) {
final filteredMap = <K, V>{};
value.forEach((key, value) {
if (test(key, value)) {
filteredMap[key] = value;
}
});
return filteredMap.reactive;
}
// Transform all values in the map
Reactive<Map<K, T>> mapValues<T>(T Function(V) transform) {
final newMap = <K, T>{};
value.forEach((key, val) {
newMap[key] = transform(val);
});
return newMap.reactive;
}
// Add all entries from another map
void addAll(Map<K, V> other) {
final newMap = Map<K, V>.from(value);
newMap.addAll(other);
value = newMap;
}
// Remove all keys that satisfy a condition
void removeKeysWhere(bool Function(K) test) {
final newMap = Map<K, V>.from(value);
newMap.removeWhere((key, _) => test(key));
value = newMap;
}
}
Set Helper Methods
// Add these extension methods to enhance ReactiveSetHelpers
extension EnhancedReactiveSetHelpers<E> on Reactive<Set<E>> {
// Compute set union with another set
Reactive<Set<E>> union(Set<E> other) {
return value.union(other).reactive;
}
// Compute set intersection with another set
Reactive<Set<E>> intersection(Set<E> other) {
return value.intersection(other).reactive;
}
// Compute set difference with another set
Reactive<Set<E>> difference(Set<E> other) {
return value.difference(other).reactive;
}
// Toggle an element (add if not present, remove if present)
void toggle(E element) {
final newSet = Set<E>.from(value);
if (newSet.contains(element)) {
newSet.remove(element);
} else {
newSet.add(element);
}
value = newSet;
}
}
Collection-Specific Middleware Examples
Fluxivity middleware can be customized to work specifically with collections. Here are some examples of collection-specific middleware implementations:
Validation Middleware for Collections
// Middleware that validates list elements before updates
class ListValidationMiddleware<E> extends FluxivityMiddleware<List<E>> {
final bool Function(E) validateElement;
final void Function(E)? onInvalidElement;
ListValidationMiddleware(this.validateElement, {this.onInvalidElement});
@override
void beforeUpdate(List<E> oldValue, List<E> newValue) {
for (final element in newValue) {
if (!validateElement(element)) {
if (onInvalidElement != null) {
onInvalidElement!(element);
}
throw ArgumentError('Invalid element: $element');
}
}
}
@override
bool shouldEmit(List<E> oldValue, List<E> newValue) {
return newValue.every(validateElement);
}
}
// Usage example:
final numbersList = [1, 2, 3].toReactive(
middlewares: [
ListValidationMiddleware<int>(
(number) => number >= 0, // Only allow positive numbers
onInvalidElement: (number) => print('Invalid number: $number'),
),
],
);
// This will work
numbersList.value.add(4);
// This will throw an error
try {
numbersList.value.add(-1);
} catch (e) {
print(e); // ArgumentError: Invalid element: -1
}
Deep Equality Middleware for Maps
// Middleware that checks for deep equality in maps
class DeepEqualityMapMiddleware<K, V> extends FluxivityMiddleware<Map<K, V>> {
@override
bool shouldEmit(Map<K, V> oldValue, Map<K, V> newValue) {
// Check if they have the same keys
if (oldValue.length != newValue.length ||
!oldValue.keys.every((key) => newValue.containsKey(key))) {
return true;
}
// Check if any value has changed deeply
for (final key in oldValue.keys) {
if (!_deepEquals(oldValue[key], newValue[key])) {
return true;
}
}
return false;
}
bool _deepEquals(dynamic a, dynamic b) {
if (a == b) return true;
if (a is Map && b is Map) {
return a.length == b.length &&
a.keys.every((key) =>
b.containsKey(key) && _deepEquals(a[key], b[key]));
}
if (a is List && b is List) {
return a.length == b.length &&
List.generate(a.length, (i) => i).every((i) =>
_deepEquals(a[i], b[i]));
}
return false;
}
}
// Usage example:
final userSettings = {
'theme': {'primary': '#007AFF', 'background': '#FFFFFF'},
'notifications': {'email': true, 'push': true},
}.toReactive(
middlewares: [DeepEqualityMapMiddleware<String, dynamic>()],
);
// This will emit an update because it's a deep change
final newTheme = Map<String, String>.from(userSettings.value['theme'] as Map<String, String>);
newTheme['primary'] = '#FF0000';
userSettings.value['theme'] = newTheme;
Change Tracking Middleware for Sets
// Middleware that tracks element additions and removals in sets
class SetChangeTrackingMiddleware<E> extends FluxivityMiddleware<Set<E>> {
final List<E> addedElements = [];
final List<E> removedElements = [];
@override
void beforeUpdate(Set<E> oldValue, Set<E> newValue) {
// Track elements that will be added
addedElements.addAll(newValue.difference(oldValue));
// Track elements that will be removed
removedElements.addAll(oldValue.difference(newValue));
}
@override
void afterUpdate(Set<E> oldValue, Set<E> newValue) {
if (addedElements.isNotEmpty) {
print('Added elements: $addedElements');
addedElements.clear();
}
if (removedElements.isNotEmpty) {
print('Removed elements: $removedElements');
removedElements.clear();
}
}
}
// Usage example:
final tagSet = {'dart', 'flutter', 'reactive'}.toReactive(
middlewares: [SetChangeTrackingMiddleware<String>()],
);
// This will track and print added elements
tagSet.value = {'dart', 'flutter', 'reactive', 'state-management'};
// Output: Added elements: [state-management]
// This will track and print removed elements
tagSet.value = {'dart', 'reactive', 'state-management'};
// Output: Removed elements: [flutter]
Persistence Middleware for Any Collection
// Middleware that persists collection changes to local storage
class PersistenceMiddleware<T> extends FluxivityMiddleware<T> {
final String storageKey;
final Function(T) serialize;
final Function(Map<String, dynamic>) deserialize;
PersistenceMiddleware({
required this.storageKey,
required this.serialize,
required this.deserialize,
});
@override
void afterUpdate(T oldValue, T newValue) {
// In a real app, you would use SharedPreferences, Hive, or other storage
final serialized = serialize(newValue);
print('Persisting to $storageKey: $serialized');
// Example persistence code (pseudocode):
// await SharedPreferences.getInstance().then((prefs) {
// prefs.setString(storageKey, jsonEncode(serialized));
// });
}
// Method to load from storage (not part of middleware interface)
Future<T?> loadFromStorage() async {
// Example load code (pseudocode):
// final prefs = await SharedPreferences.getInstance();
// final data = prefs.getString(storageKey);
// if (data != null) {
// return deserialize(jsonDecode(data));
// }
return null;
}
}
// Usage example with a list:
final todoList = <String>[].toReactive(
middlewares: [
PersistenceMiddleware<List<String>>(
storageKey: 'todo_list',
serialize: (list) => {'items': list},
deserialize: (data) => (data['items'] as List).cast<String>(),
),
],
);
// Adding an item will trigger persistence
todoList.value.add('Buy groceries');
todoList.value = List.from(todoList.value); // Force notification
Advanced Collection Patterns
You can combine reactive collections with computed values and middleware to create powerful reactive patterns:
Form Validation System
// A form validation system using reactive maps
void main() {
// Field values
final formFields = <String, String>{
'name': '',
'email': '',
'age': '',
}.reactive;
// Validation rules
final validators = <String, bool Function(String)>{
'name': (value) => value.length >= 2,
'email': (value) => value.contains('@') && value.contains('.'),
'age': (value) => int.tryParse(value) != null && int.parse(value) >= 18,
};
// Computed validation errors
final validationErrors = Computed<Map<String, String?>>([formFields], (sources) {
final fields = sources[0].value as Map<String, String>;
final errors = <String, String?>{};
fields.forEach((key, value) {
if (value.isEmpty) {
errors[key] = null; // No error for empty fields until submitted
} else if (!validators[key]!(value)) {
errors[key] = 'Invalid $key';
} else {
errors[key] = null; // No error
}
});
return errors;
});
// Computed form validity
final isFormValid = Computed<bool>([formFields, validationErrors], (sources) {
final fields = sources[0].value as Map<String, String>;
final errors = sources[1].value as Map<String, String?>;
// Form is valid if all fields have values and no errors
return fields.values.every((v) => v.isNotEmpty) &&
errors.values.every((e) => e == null);
});
// Submit function
void submitForm() {
if (isFormValid.value) {
print('Form submitted: ${formFields.value}');
} else {
print('Cannot submit, form is invalid');
}
}
// UI would update these values
formFields.value = {
'name': 'John Doe',
'email': 'john@example.com',
'age': '25',
};
print('Validation errors: ${validationErrors.value}');
print('Is form valid: ${isFormValid.value}');
submitForm();
}
Filtered and Sorted Collection View
// Creating filtered and sorted views of collections
void main() {
// Original todo items
final todos = [
{'id': 1, 'text': 'Buy groceries', 'completed': false, 'priority': 2},
{'id': 2, 'text': 'Call mom', 'completed': true, 'priority': 1},
{'id': 3, 'text': 'Finish project', 'completed': false, 'priority': 3},
{'id': 4, 'text': 'Pay bills', 'completed': false, 'priority': 2},
].reactive;
// Filter settings
final showCompleted = false.reactive;
// Sort settings
final sortByPriority = true.reactive;
// Computed filtered list
final filteredTodos = Computed<List<Map<String, dynamic>>>([todos, showCompleted], (sources) {
final allTodos = sources[0].value as List<Map<String, dynamic>>;
final includeCompleted = sources[1].value as bool;
if (includeCompleted) {
return List.from(allTodos);
} else {
return allTodos.where((todo) => todo['completed'] == false).toList();
}
});
// Computed sorted and filtered list
final sortedFilteredTodos = Computed<List<Map<String, dynamic>>>(
[filteredTodos, sortByPriority],
(sources) {
final filtered = List<Map<String, dynamic>>.from(sources[0].value);
final byPriority = sources[1].value as bool;
if (byPriority) {
filtered.sort((a, b) => (b['priority'] as int).compareTo(a['priority'] as int));
} else {
filtered.sort((a, b) => (a['id'] as int).compareTo(b['id'] as int));
}
return filtered;
}
);
// Display initial result
print('Initial todos:');
sortedFilteredTodos.value.forEach((todo) {
print('${todo['text']} (Priority: ${todo['priority']})');
});
// Change filter settings
showCompleted.value = true;
// Display after filter change
print('\nAfter showing completed:');
sortedFilteredTodos.value.forEach((todo) {
final status = todo['completed'] ? '✓' : '○';
print('$status ${todo['text']} (Priority: ${todo['priority']})');
});
// Change sort settings
sortByPriority.value = false;
// Display after sort change
print('\nAfter sorting by ID:');
sortedFilteredTodos.value.forEach((todo) {
final status = todo['completed'] ? '✓' : '○';
print('$status ${todo['text']} (ID: ${todo['id']})');
});
}
Shopping Cart System
// A shopping cart system using reactive collections
void main() {
// Product catalog
final products = {
'p1': {'name': 'T-shirt', 'price': 19.99, 'inventory': 10},
'p2': {'name': 'Jeans', 'price': 49.99, 'inventory': 5},
'p3': {'name': 'Hat', 'price': 14.99, 'inventory': 3},
}.reactive;
// Shopping cart - map of product IDs to quantities
final cart = <String, int>{}.reactive;
// Computed cart details
final cartDetails = Computed<List<Map<String, dynamic>>>([cart, products], (sources) {
final cartMap = sources[0].value as Map<String, int>;
final productMap = sources[1].value as Map<String, Map<String, dynamic>>;
return cartMap.entries.map((entry) {
final productId = entry.key;
final quantity = entry.value;
final product = productMap[productId]!;
return {
'id': productId,
'name': product['name'],
'price': product['price'],
'quantity': quantity,
'total': (product['price'] as double) * quantity,
};
}).toList();
});
// Computed cart total
final cartTotal = Computed<double>([cartDetails], (sources) {
final details = sources[0].value as List<Map<String, dynamic>>;
return details.fold(0.0, (sum, item) => sum + (item['total'] as double));
});
// Helper to add a product to cart
void addToCart(String productId, [int quantity = 1]) {
// Check inventory
final inventory = (products.value[productId]?['inventory'] as int?) ?? 0;
final currentQuantity = cart.value[productId] ?? 0;
if (inventory >= currentQuantity + quantity) {
final newCart = Map<String, int>.from(cart.value);
newCart[productId] = currentQuantity + quantity;
cart.value = newCart;
// Update inventory
final updatedProduct = Map<String, dynamic>.from(products.value[productId]!);
updatedProduct['inventory'] = inventory - quantity;
final newProducts = Map<String, Map<String, dynamic>>.from(products.value);
newProducts[productId] = updatedProduct;
products.value = newProducts;
print('Added ${products.value[productId]?['name']} to cart');
} else {
print('Not enough inventory for ${products.value[productId]?['name']}');
}
}
// Helper to remove a product from cart
void removeFromCart(String productId, [int quantity = 1]) {
final currentQuantity = cart.value[productId] ?? 0;
if (currentQuantity <= 0) return;
final newCart = Map<String, int>.from(cart.value);
if (currentQuantity <= quantity) {
newCart.remove(productId);
} else {
newCart[productId] = currentQuantity - quantity;
}
cart.value = newCart;
// Update inventory
final inventory = (products.value[productId]?['inventory'] as int?) ?? 0;
final quantityToReturn = currentQuantity < quantity ? currentQuantity : quantity;
final updatedProduct = Map<String, dynamic>.from(products.value[productId]!);
updatedProduct['inventory'] = inventory + quantityToReturn;
final newProducts = Map<String, Map<String, dynamic>>.from(products.value);
newProducts[productId] = updatedProduct;
products.value = newProducts;
print('Removed ${products.value[productId]?['name']} from cart');
}
// Simulating user actions
addToCart('p1', 2);
addToCart('p2', 1);
// Display cart status
print('\nCart contents:');
cartDetails.value.forEach((item) {
print('${item['name']} x ${item['quantity']} = \$${item['total'].toStringAsFixed(2)}');
});
print('Total: \$${cartTotal.value.toStringAsFixed(2)}');
// Update an item quantity
addToCart('p1', 1);
// Display updated cart
print('\nUpdated cart:');
cartDetails.value.forEach((item) {
print('${item['name']} x ${item['quantity']} = \$${item['total'].toStringAsFixed(2)}');
});
print('Total: \$${cartTotal.value.toStringAsFixed(2)}');
// Check inventory
print('\nInventory status:');
products.value.forEach((id, product) {
print('${product['name']}: ${product['inventory']} left');
});
}
Example Use Cases of Fluxivity
Read more examples at example/README.md
Advanced Usage
The following is a list of functions that are not part of regular day to day usage, but are useful in certain scenarios where you want to have extended control over how fluxivity functions.
Middlewares
Middlewares allow you to extend the package with custom functionality. Fluxivity Middlewares should extend the FluxivityMiddleware
class and implement the methods in them. Other than the shouldEmit
method, the other methods return void. The shouldEmit method returns a boolean value which controls whether the stream should emit the new value or not. This can be used to implement custom logic to control when the stream should emit a new value. Middlewares function for both Reactive and Computed classes. The extension methods since they rely on the actual reactive class get these by extension. However you will have to use the toReactive
method to pass in the middleware instead of the .reactive
extension method.
onError
The onError method of the plugin is used only within the Computed class to handle errors arising out of the compute function. In the case of error, the value of the computed method does not change and the error is passed to the onError method of the middleware. The middleware can then handle the error as required. You can write a custom error handler to deal with this case.
Example
import 'package:fluxivity/fluxivity.dart';
class LoggingMiddleware<T> extends FluxivityMiddleware<T> {
@override
void beforeUpdate(T oldValue, T newValue) {
print("Before update: $oldValue -> $newValue");
}
@override
void afterUpdate(T oldValue, T newValue) {
print("After update: $oldValue -> $newValue");
}
@override
void onError(Object error, StackTrace stackTrace) {
print("Error: $error\n$stackTrace");
}
@override
bool shouldEmit(T oldValue, T newValue) {
return true;
}
}
To use the middleware in your code, you can initialise a reactive with the middleware as follows:
Reactive<String> reactive = Reactive('test', middlewares: [LoggingMiddleware<String>()]);
Batched Updates
Fluxivity allows you to listen to updates in a batched mode instead of listening to individual events. This is useful when you want to perform multiple operations on a reactive value and only want to listen to the final value. This batched updates also work alongside the middlewares. If a middleware prevents the stream from emitting, the batched update will not be emitted. Batched updates can be used with both Reactive and Computed classes. It is also available for all extension methods. You can also nest batched updates, in which case, the updates are only emitted when the outermost batched update is ended.
final reactive = Reactive<int>(0);
// Start a batch update
reactive.startBatchUpdate();
// Perform multiple updates
reactive.value = 1;
reactive.value = 2;
reactive.value = 3;
reactive.value = 4;
// End the batch update, which will emit the last buffered value
reactive.endBatchUpdate(); // Output: 4
// Start a batch update
reactive.startBatchUpdate();
reactive.value = 5;
reactive.value = 6;
reactive.value = 7;
reactive.value = 8;
// End the batch update, which will emit the last buffered value
reactive.endBatchUpdate(publishAll: true); // Output: 5, 6, 7, 8
Memoization
Fluxivity provides a memoization utility for Computed
instances to optimize performance by caching computation results. This is especially useful for expensive computations that depend on reactive values that might change to the same values repeatedly.
Basic Usage
The memoize
function creates a memoized version of a Computed
instance that caches the results based on the values of its source reactives:
import 'package:fluxivity/fluxivity.dart';
// Create a normal computed instance
final count = Reactive(0);
final expensiveComputation = Computed([count], (sources) {
// This could be an expensive operation
print('Computing value...');
return sources[0].value * 100;
});
// Create a memoized version with a cache size of 5
final memoizedComputation = memoize(expensiveComputation, cacheSize: 5);
// When accessed with the same input values, the cached result is returned
memoizedComputation.value; // Prints: "Computing value..." and returns 0
memoizedComputation.value; // Returns 0 without printing (cached)
// When the source value changes, it recomputes
count.value = 1;
memoizedComputation.value; // Prints: "Computing value..." and returns 100
// If we change back to a previously computed value, it uses the cache
count.value = 0;
memoizedComputation.value; // Returns 0 without printing (cached)
The memoization implementation uses an LRU cache strategy
// Create with different cache sizes based on your needs
final smallCache = memoize(computed, cacheSize: 1); // Default size
final mediumCache = memoize(computed, cacheSize: 10);
final largeCache = memoize(computed, cacheSize: 100);
When to Use Memoization
Memoization is most beneficial when:
- The computation is expensive (complex calculations, string formatting, etc.)
- The input values repeat frequently
- The computed output is used in multiple places
For simple computations or when values rarely repeat, the overhead of memoization might outweigh its benefits.
Usage along with storage providers
Fluxivity can be used along with existing storage providers like Hive, Shared Preferences, etc. The following is an example of how you can use Fluxivity along with Hive. This assumes you have setup Hive and its type adapters, you could create a custom class called HiveReactive which extends the Reactive class and implements the methods to save and load from Hive.
import 'package:fluxivity/core/reactive.dart';
import 'package:hive/hive.dart';
class HiveReactive<T> extends Reactive<T> {
final String boxName;
final String key;
HiveReactive(this.boxName, this.key, T initialValue) : super(initialValue) {
_loadFromHive();
}
@override
set value(T newValue) {
super.value = newValue;
_saveToHive();
}
Future<void> _loadFromHive() async {
final box = await Hive.openBox<T>(boxName);
if (box.containsKey(key)) {
super.value = box.get(key);
}
}
Future<void> _saveToHive() async {
final box = await Hive.openBox<T>(boxName);
box.put(key, value);
}
}
License
MIT License at LICENSE
Libraries
- core/computed
- core/memoized_computed
- core/reactive
- core/reactive_list_simplified
- core/reactive_map
- core/reactive_set
- core/snapshot
- fluxivity
- A lightweight reactive state management library for Dart applications.
- plugins/plugin_abstract
- plugins/plugins
- plugins/prebuilt/logger_middleware