flutter_reactter 6.0.0
flutter_reactter: ^6.0.0 copied to clipboard
A light, powerful and quick Reactive State Management, Dependency Injection and Event Management.
A light, powerful and quick Reactive State Management, Dependency Injection and Event Management.
Features #
- ⚡️ Engineered for Speed.
- ⚖️ Super Lightweight(🥇 See benchmarks).
- 📏 Reduce Boilerplate Code significantly(🥇 See benchmarks).
- 📝 Improve Code Readability.
- 💧 Flexible and Adaptable to any architecture.
- ☢️ Reactive States using Signal.
- ♻️ Reusable States creating Custom hooks.
- 🎮 Total Control to re-render widget tree.
- 🧪 Fully Testable, 100% code coverage.
- 🪄 Zero Configuration and No Code Generation necessary.
- 💙 Compatible with Dart and Flutter, supports the latest version of Dart.
Let's see a small and simple example:
// Create a reactive state using `Signal`
final count = 0.signal;
void main() {
// Put on listen `didUpdate` event, whitout use `Stream`
Reactter.on(count, Lifecycle.didUpdate, (_, __) => print('Count: $count'));
// Change the `value` in any time(e.g., each 1 second).
Timer.periodic(Duration(seconds: 1), (_) => count.value++);
// And you can use in flutter, e.g:
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ReactterWatcher(
builder: (context, child) {
// This will be re-built, at each count change.
return Text(
"$count",
style: Theme.of(context).textTheme.headline3,
);
},
),
),
),
),
);
}
Clean and easy!
See more examples here!
Contents #
- Quickstart
- About Reactter
- State management
- Dependency injection
- Builder
- Factory
- Singleton
- Shortcuts to manage instances
- UseInstance
- ReactterProvider (
flutter_reactter) - ReactterProviders (
flutter_reactter) - ReactterComponent (
flutter_reactter)
- LifeCycle and event management
- Shortcuts to manage events
- UseEffect
- ReactterConsumer (
flutter_reactter) - ReactterWatcher (
flutter_reactter) - BuildContext extension (
flutter_reactter)
- Custom hooks
- Lazy state
- Generic arguments
- Memo
- Difference between Signal and UseState
- Resources
- Contribute
- Authors
Quickstart #
Before anything, you need to be aware that Reactter is distributed on two packages, with slightly different usage.
The package of Reactter that you will want to install depends on the type of project you are working on.
Select one of the following options to know how to install it:
Dart only
Add the package on your project.
-
Using command:
dart pub add reactter -
Or put directly into
pubspec.yamlfile:dependencies: reactter: #add version hereand run
dart pub get.
Now in your Dart code, you can use:
import 'package:reactter/reactter.dart';
Flutter
Add the package on your project.
-
Using command:
flutter pub add flutter_reactter -
Or put directly into
pubspec.yamlfile:dependencies: flutter_reactter: #add version hereand run
flutter pub get.
Now in your Dart code, you can use:
import 'package:flutter_reactter/flutter_reactter.dart';
And it is recommended to use
which will help to encourage good coding practices and prevent frequent problems using the Reactter convensions.
If you use Visual Studio Code, it is a good idea to use Reactter Snippets for improving productivity.
About Reactter #
Reactter is a light and powerful solution for Dart and Flutter. It is composed of three main concepts that can be used together to create maintainable and scalable applications, which are:
State management #
In Reactter, state is understood as any object that extends ReactterState, endowing it with capabilities such as the ability to store one or more values and to broadcast notifications of its changes.
Reactter offers the following several state managers:
NOTE: The hooks (also known as
ReactterHook) are named with the prefixUseaccording to convention.
RECOMMENDED: See also difference between Signal and UseState and about custom hooks.
Signal #
Signal is an object (that extends ReactterState) which has a value and notifies about its changes.
It can be initialized using the extension .signal:
final intSignal = 0.signal;
final strSignal = "initial value".signal;
final userSignal = User().signal;
or using the constructor class Signal<T>(T initialValue):
final intSignal = Signal<int>(0);
final strSignal = Signal("initial value");
final userSignal = Signal(User());
Signal has a value property that allows to read and write its state:
intSignal.value = 10;
print("Current state: ${intSignal.value}");
or also can use the callable function:
intSignal(10);
print("Current state: ${intSignal()}");
or simply use .toString() implicit to get its value as String:
print("Current state: $intSignal");
NOTE:
Signalnotifies that itsvaluehas changed when the previousvalueis different from the currentvalue. If itsvalueis anObject, it does not detect internal changes, only whenvalueis setted to anotherObject.
Use update method to notify changes after run a set of instructions:
userSignal.update((user) {
user.firstname = "Firstname";
user.lastname = "Lastname";
});
Use refresh method to force to notify changes.
userSignal.refresh();
When value has changed, the Signal will emit the following events(learn about it here):
Lifecycle.willUpdateevent is triggered before thevaluechange orupdate,refreshmethods have been invoked.Lifecycle.didUpdateevent is triggered after thevaluechange orupdate,refreshmethods have been invoked.
NOTE: When you do any arithmetic operation between two
Signals, it returns anObj, for example:1.signal + 2.signalreturns3.obj. AnObjis like aSignalwithout reactive functionality, but you can convert it toSignalusing.toSignal.
NOTE: In flutter, using
ReactterWatcher, is a way to keep the widgets automatically updates, accessing the value of signal reactively.
UseState #
UseState is a hook(ReactterHook) that allows to declare state variables and manipulate its value, which in turn notifies about its changes.
UseState<T>(T initialValue)
UseState accepts a property:
initialValue: is a unique value of any type that you use to initialize the state.
It can be declared inside a class, like this:
class CounterController {
final count = UseState(0);
}
NOTE: if your variable hook is
lateuseReactter.lazyState. Learn about it here.
UseState has a value property that allows to read and write its state:
class CounterController {
final count = UseState(0);
CounterController() {
print("Prev state: ${count.value}");
count.value = 10;
print("Current state: ${count.value}");
}
}
NOTE:
UseStatenotifies that itsvaluehas changed when the previousvalueis different from the currentvalue. If itsvalueis anObject, it does not detect internal changes, only whenvalueis setted to anotherObject.
Use update method to notify changes after run a set of instructions:
userState.update((user) {
user.firstname = "Firstname";
user.lastname = "Lastname";
});
Use refresh method to force to notify changes.
userState.refresh();
When value has changed, the UseState will emitted the following events(learn about it here):
Lifecycle.willUpdateevent is triggered before thevaluechange orupdate,refreshmethods have been invoked.Lifecycle.didUpdateevent is triggered after thevaluechange orupdate,refreshmethods have been invoked.
UseAsyncState #
UseAsyncState is a hook (ReactterHook) with the same feature as UseState but its value will be lazily resolved by a function(asyncFunction).
UseAsyncState<T>(
T initialValue,
Future<T> asyncFunction(),
);
UseAsyncState accepts theses properties:
initialValue: is a unique value of any type that you use to initialize the state.asyncFunction: is a function that will be called by theresolvedmethod and sets the value of the state.
Use UseAsyncState.withArg to pass a argument to the asyncFunction.
UseAsyncState.withArg<T, A>(
T initialValue,
Future<T> asyncFunction(A) ,
)
NOTE: if your variable hook is
lateuseReactter.lazyState. Learn about it here.
This is a translate example:
class TranslateController {
final translateState = UseAsyncStates.withArg(
null,
(ArgsX3<String> args) async {
final text = args.arg;
final from = args.arg2;
final to = args.arg3;
// this is fake code, which simulates a request to API
return await api.translate(text, from, to);
}
);
TranslateController() {
translateState.resolve(
Args3('Hello world', 'EN','ES'),
).then((_) {
print("'Hello world' translated to Spanish: '${translateState.value}'");
});
}
}
RECOMMENDED: If you wish to optimize the state resolution, the best option is to use the memoization technique. Reactter provides this using
Memo(Learn about it here), e.g:[...] final translateState = UseAsyncState.withArg<String?, ArgsX3<String>>( null, /// `Memo` stores the value resolved in cache, /// and retrieving that same value from the cache the next time /// it's needed instead of resolving it again. Memo.inline( (ArgsX3<String> args) async { final text = args.arg; final from = args.arg2; final to = args.arg3; // this is fake code, which simulates a request to API return await api.translate(text, from, to); }, AsyncMemoSafe(), // avoid to save in cache when throw a error ), ); [...]
RECOMMENDED: In the above example uses
Args(generic arguments), but using Record instead is recommended if your project supports it.
Use the when method to return a computed value depending on it's state:
final computedValue = asyncState.when<String>(
standby: (value) => "🔵 Standby: $value",
loading: (value) => "⏳ Loading...",
done: (value) => "✅ Resolved: $value",
error: (error) => "❌ Error: $error",
);
When value has changed, the UseAsyncState will emit the following events (learn about it here):
Lifecycle.willUpdateevent is triggered before thevaluechange orupdate,refreshmethods have been invoked.Lifecycle.didUpdateevent is triggered after thevaluechange orupdate,refreshmethods have been invoked.
UseReducer #
UseReducer is a hook(ReactterHook) that manages state using reducer method. An alternative to UseState.
RECOMMENDED:
UseReduceris usually preferable overUseStatewhen you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
UseReducer<T>(
T reducer(T state, ReactterAction<dynamic> action),
T initialState,
);
UseReducer accepts two properties:
reducer: is a method contains your custom state logic that calculates the new state using current state, and actions.initialState: is a unique value of any type that you use to initialize the state.
UseReducer exposes a dispatch method that allows you to invoke the reducer method sending a ReactterAction.
The current state can be accessed through the value property.
Here's the counter example using UseReducer:
class Store {
final int count;
Store({this.count = 0});
}
Store reducer(Store state, ReactterAction<int?> action) {
switch (action.type) {
case 'INCREMENT':
return Store(count: state.count + (action.payload ?? 1));
case 'DECREMENT':
return Store(count: state.count + (action.payload ?? 1));
default:
throw UnimplementedError();
}
}
class CounterController {
final useCounter = UseReducer(reducer, Store(count: 0));
CounterController() {
print("count: ${useCounter.value.count}"); // count: 0;
useCounter.dispatch(ReactterAction(type: 'INCREMENT', payload: 2));
print("count: ${useCounter.value.count}"); // count: 2;
useCounter.dispatch(ReactterAction(type: 'DECREMENT'));
print("count: ${useCounter.value.count}"); // count: 1;
}
}
The actions can be created as a callable class, extending from ReactterActionCallable and used as follows:
class IncrementAction extends ReactterActionCallable<Store, int> {
IncrementAction([int quantity = 1]) : super(
type: 'INCREEMNT', payload: quantity
);
@override
Store call(Store state) => Store(count: state.count + payload);
}
class DecrementAction extends ReactterActionCallable<Store, int> {
DecrementAction([int quantity = 1]) : super(
type: 'DECREMENT', payload: quantity
);
@override
Store call(Store state) => Store(count: state.count - payload);
}
Store reducer(Store state, ReactterAction action) {
if (action is ReactterActionCallable) return action(state);
return UnimplementedError();
}
class CounterController {
final useCounter = UseReducer(reducer , Store(count: 0));
CounterController() {
print("count: ${useCounter.value.count}"); // count: 0;
useCounter.dispatch(IncrementAction(2));
print("count: ${useCounter.value.count}"); // count: 2;
useCounter.dispatch(DecrementAction());
print("count: ${useCounter.value.count}"); // count: 1;
}
}
When value has changed, the UseReducer will emit the following events (learn about it here):
Lifecycle.willUpdateevent is triggered before thevaluechange orupdate,refreshmethods have been invoked.Lifecycle.didUpdateevent is triggered after thevaluechange orupdate,refreshmethods have been invoked.
UseCompute #
UseCompute is a hook(ReactterHook) that keeps listening for state dependencies changes, to return a computed value(T) from a defined method(computeValue).
UseCompute<T>(
T computeValue(),
List<ReactterState> dependencies,
)
UseCompute accepts two parameters:
computeValue: is a method is called whenever there is a change in any of thedependencies, and it is responsible for calculating and setting the computed value.dependencies: is a list of states thatUseComputekeeps an active watch on, listening for any changes that may occur for calling thecomputeValuefunction.
so, here an example:
class AppController {
final stateA = UseState(1);
final stateB = UseState(7);
late final computeState = Reactter.lazyState(
() => UseCompute(
// The `clamp` is a method that returns this num clamped
// to be in the range lowerLimit-upperLimit(e.g., 10-15).
() => addAB().clamp(10, 15),
[stateA, stateB],
),
);
int addAB() => stateA.value + stateB.value;
void printResult() => print("${addAB()} -> ${computeState.value}");
AppController() {
printResult(); // 8 -> 10
stateA.value += 1; // Will not notify change
printResult(); // 9 -> 10
stateB.value += 2; // Will notify change
printResult(); // 11 -> 11
stateA.value += 6; // Will notify change
printResult(); // 17 -> 15
stateB.value -= 1; // Will not notify change
printResult(); // 16 -> 15
stateA.value -= 8; // Will notify change
printResult(); // 8 -> 10
}
}
UseCompute has a value property which represents the computed value.
NOTE:
UseComputenotifies that itsvaluehas changed when the previousvalueis different from the currentvalue.
When value has changed, the UseState will emit the following events (learn about it here):
Lifecycle.willUpdateevent is triggered before thevaluechange orupdate,refreshmethods have been invoked.Lifecycle.didUpdateevent is triggered after thevaluechange orupdate,refreshmethods have been invoked.
NOTE:
UseComputeis read-only, meaning that its value cannot be changed, except by invoking thecomputeValuemethod.
RECOMENDED:
UseComputedoes not cache the computed value, meaning it recalculates the value with every change in its dependencies , potentially impacting performance, especially if the computation is expensive. In these cases, you should consider usingMemo(leard about it here) in the following manner:
late final myUseComputeMemo = Reactter.lazyState((){
final addAB = Memo(
(Args2 args) => args.arg1 + args.arg2,
);
return UseCompute(
() => addAB(
Args2(stateA.value, stateB.value),
),
[stateA, stateB],
),
}, this);
Dependency injection #
With Reactter, you can create, delete and access the desired object from a single location, and you can do it from anywhere in the code, thanks to reactter's dependency injection system.
Dependency injection offers several benefits. It promotes the principle of inversion of control, where the control over object creation and management is delegated to Reactter. This improves code modularity, reusability, and testability. It also simplifies the code by removing the responsibility of creating dependencies from individual classes, making them more focused on their core functionality.
Reactter has three ways to manage an instance, which are:
Reactter offers the following several instance managers:
by flutter_reactter:
Builder #
Builder is a ways to manage an instance, which registers a builder function and creates the instance, unless it has already done so.
In builder mode, when the dependency tree no longer needs it, it is completely deleted, including deregistration (deleting the builder function).
Reactter identifies the builder mode as InstanceManageMode.builder and it's using for default.
NOTE: Builder uses less RAM than Factory and Singleton, but it consumes more CPU than the other modes.
Factory #
Factory is a ways to manage an instance, which registers a builder function only once and creates the instance if not already done.
In factory mode, when the dependency tree no longer needs it, the instance is deleted and the builder function is kept in the register.
Reactter identifies the factory mode as InstanceManageMode.factory and to active it, set it in the mode argument of Reactter.register and Reactter.create, or use Reactter.lazyStateFactory, Reactter.factory.
NOTE: Factory uses more RAM than Builder but not more than Singleton, and consumes more CPU than Singleton but not more than Builder.
Singleton #
Singleton is a ways to manage a instance, which registers a builder function and creates the instance only once.
The singleton mode preserves the instance and its states, even if the dependency tree stops using it.
Reactter identifies the singleton mode as InstanceManageMode.singleton and to active it, set it in the mode argument of Reactter.register and Reactter.create, or use Reactter.lazyStateSingleton, Reactter.singleton.
NOTE: Use
Reactter.destroyif you want to force destroy the instance and its register.
NOTE: Singleton consumes less CPU than Builder and Factory, but uses more RAM than the other modes.
Shortcuts to manage instances #
Reactter offers several convenient shortcuts for managing instances:
Reactter.register: Registers a builder function, for creating a new instance using[Reactter|UseInstance].[get|create|builder|factory|singleton].Reactter.lazyStateBuilder: Registers a builder function, for creating a new instance as Builder mode using[Reactter|UseInstance].[get|create|builder].Reactter.lazyStateFactory: Registers a builder function, for creating a new instance as Factory mode using[Reactter|UseInstance].[get|create|factory].Reactter.lazyStateSingleton: Registers a builder function, for creating a new instance as Singleton mode using[Reactter|UseInstance].[get|create|singleton].Reactter.create: Registers, creates and returns the instance directly.Reactter.builder: Registers, creates and returns the instance as Builder directly.Reactter.factory: Registers, creates and returns the instance as Factory directly.Reactter.singleton: Registers, creates and returns the instance as Singleton directly.Reactter.get: Returns a previously created instance or creates a new instance from the builder function registered by[Reactter|UseInstance].[register|lazyBuilder|lazyFactory|lazySingleton].Reactter.delete: Deletes the instance but keeps the builder function.Reactter.unregister: Removes the builder function, preventing the creation of the instance.Reactter.destroy: Destroys the instance and the builder function.Reactter.find: Gets the instance.Reactter.isRegistered: Checks if an instance is registered in Reactter.Reactter.getInstanceManageMode: ReturnsInstanceManageModeof the instance sent.
In each of the events methods shown above (except Reactter.isRegister and Reactter.getInstanceManageMode), it provides the id argument for managing the instances of the same type by a unique identity.
NOTE: The scope of the registered instances is global. This indicates that using the shortcuts to manage instance or
UseInstancewill allow you to access them from anywhere in the project.
UseInstance #
UseInstance is a hook(ReactterHook) that allows to manage an instance.
UseInstance<T>([String? id]);
The default constructor uses Reactter.find to get the instance of the T type with or without id that is available.
NOTE: The instance that you need to get, must be created by
Dependency injectionbefore.
Use instance getter to get the instance.
Here an example using UseIntance:
class AppController {
final useAuthController = UseInstance<AuthController>();
// final useOtherControllerWithId = UseInstance<OtherController>("UniqueId");
AuthController? authController = useAuthController.instance;
AppController() {
UseEffect(() {
authController = useAuthController.instance;
}, [useAuthController]);
}
}
NOTE: In the example above uses
UseEffecthook, to wait for theinstanceto become available.
UseInstance provides some constructors and factories for managing an instance, which are:
UseInstance.register: Registers a builder function, for creating a new instance using[Reactter|UseInstance].[get|create|builder|factory|singleton].UseInstance.lazyBuilder: Registers a builder function, for creating a new instance as Builder mode using[Reactter|UseInstance].[get|create|builder].UseInstance.lazyFactory: Registers a builder function, for creating a new instance as Factory mode using[Reactter|UseInstance].[get|create|factory].UseInstance.lazySingleton: Registers a builder function, for creating a new instance as Singleton mode using[Reactter|UseInstance].[get|create|singleton].UseInstance.create: Registers, creates and returns the instance directly.UseInstance.builder: Registers, creates and returns the instance as Builder directly.UseInstance.factory: Registers, creates and returns the instance as Factory directly.UseInstance.singleton: Registers, creates and returns the instance as Singleton directly.UseInstance.get: Returns a previously created instance or creates a new instance from the builder function registered by[Reactter|UseInstance].[register|lazyBuilder|lazyFactory|lazySingleton].
In each of the contructors or factories above shown, it provides the id property for managing the instances of the same type by a unique identity.
NOTE: The scope of the registered instances is global. This indicates that using the shortcuts to manage instance or
UseInstancewill allow you to access them from anywhere in the project.
ReactterProvider #
ReactterProvider is a Widget (exclusive of flutter_reactter) that hydrates from an instance of T type to the Widget tree.
ReactterProvider<T>(
T instanceBuilder(), {
String? id,
bool init = false,
InstanceManageMode type = InstanceManageMode.builder,
Widget? child,
required Widget builder(T instance, BuilderContext context, Widget? child),
})
ReactterProvider accepts theses properties:
-
instanceBuilder: to define a method for the creation of a new instance ofTtype.NOTE: The instance can be accessed through methods BuildContext extension.
RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
-
id: to uniquely identify the instance. -
init: to indicate that the instance must be initialized before theReactterProvideris mounted. -
mode: to determine the instance manage mode(Builder, Factory or Singleton). -
child: to pass aWidgetthrough thebuildermethod that it will be built only once. -
builder: to define a method that contains the builder logic of the widget that will be embedded in the widget tree. This method exposes theinstance(T) created, a newcontext(BuildContext) and achild(Widget) defined in thechildproperty.
Here an example:
ReactterProvider<CounterController>(
() => CounterController(),
child: const Text('This widget is rendered once'),
builder: (counterController, context, child) {
// `context.watch` listens any CounterController changes for rebuild this widget tree.
context.watch<CounterController>();
// Change the `value` each 1 second.
Timer.periodic(Duration(seconds: 1), (_) => counterController.count.value++);
return Column(
children: [
child!, // The child widget has already been built in `child` property.
Text("count: ${counterController.count.value}"),
],
);
},
)
NOTE:
ReactteProvideris "scoped". So, thebuildermethod will be rebuild when the instance or anyReactterStatespecified the watch methods of BuildContext extension changes.
ReactterProviders #
ReactterProviders is a Widget (exclusive of flutter_reactter) that allows to use multiple ReactterProvider in a nested way.
ReactterProviders(
[
ReactterProvider(
() => AppController(),
),
ReactterProvider(
() => ConfigContext(),
id: 'App',
),
ReactterProvider(
() => ConfigContext(),
id: 'Dashboard'
),
],
builder: (context, child) {
final appController = context.use<AppController>();
final appConfigContext = context.use<ConfigContext>('App');
final dashboardConfigContext = context.use<ConfigContext>('Dashboard');
...
},
)
RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
NOTE:
ReactteProvidersis "scoped". So, thebuildermethod will be rebuild when the instance or anyReactterStatespecified using the watch methods of BuildContext extension changes.
ReactterComponent #
ReactterComponent is a abstract StatelessWidget (exclusive of flutter_reactter) that provides ReactterProvider features, whose instance of T type is exposed trough render method.
class CounterComponent extends ReactterComponent<CounterController> {
const CounterComponent({Key? key}) : super(key: key);
@override
get builder => () => CounterController();
@override
void listenStates(counterController) => [counterController.count];
@override
Widget render(counterController, context) {
return Text("Count: ${counterController.count.value}");
}
}
Use builder getter to define the instance builder function.
RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
NOTE: If you don't use
buildergetter, the instance will not be created. Instead, an attempt will be made to locate it within the closest ancestor where it was initially created.
Use the id getter to identify the instance of T:
Use the listenStates getter to define the states that will rebuild the tree of the widget defined in the render method whenever it changes.
Use the listenAll getter as true to listen to all the instance changes to rebuild the Widget tree defined in the render method.
LifeCycle and event management #
In Reactter, the states (ReactterState) and the instances (managed by the dependency injection) contain different stages, also known as Lifecycle. This lifecycles linked events, which are:
Lifecycle.registered: This event is triggered when the instance has been registered.Lifecycle.unregistered: This event is triggered when the instance is no longer registered.Lifecycle.initialized: This event is triggered when the instance has been initialized.Lifecycle.willMount: This event(exclusive offlutter_reactter) happens when the instance is going to be mounted in the widget tree.Lifecycle.didMount: This event(exclusive offlutter_reactter) happens after the instance has been successfully mounted in the widget tree.Lifecycle.willUpdate: This event is triggered anytime the instance's state is about to be updated. The event parameter is aReactterState.Lifecycle.didUpdate: This event is triggered anytime the instance's state has been updated. The event parameter is aReactterState.Lifecycle.willUnmount: This event(exclusive offlutter_reactter) happens when the instance is about to be unmounted from the widget tree.Lifecycle.destroyed: This event is triggered when the instance has been destroyed.
Reactter offers the following several event managers:
by flutter_reactter:
Shortcuts to manage events #
Reactter offers several convenient shortcuts for managing events:
-
Reactter.on: Turns on the listen event. When theeventofinstanceis emitted, thecallbackis called:Reactter.on<T, P>(Object instance, Enum event, callback(T inst, P params)); -
Reactter.one: Turns on the listen event for only once. When theeventofinstanceis emitted, thecallbackis called and then removed.Reactter.one<T, P>(Object instance, Enum event, callback(T inst, P param)); -
Reactter.off: Removes thecallbackfromeventofinstance.Reactter.off<T, P>(Object instance, Enum event, callback(T instance, P param)); -
Reactter.offAll: Removes all events ofinstance.Reactter.offAll(Object instance);IMPORTANT: Don't use it, if you're not sure. Because it will remove all events, even those events that Reactter needs to work properly. Instead, use
Reactter.offto remove the specific events. -
Reactter.emit: Triggers aneventofinstancewith or without theparamgiven.Reactter.emit(Object instance, Enum event, [dynamic param]); -
Reactter.emitAsync: Triggers aneventofinstancewith or without theparamgiven as async way.Future<void> Reactter.emitAsync(Object instance, Enum event, [dynamic param]);
In each of the methods it receives as first parameter an instance that can be directly the instance object or use ReactterInstance instead:
void onDidUpdate(inst, state) => print("Instance: $inst, state: $state");
final appController = Reactter.get<AppController>();
// or using `ReactterIntance`
final appController = ReactterInstance<AppController>();
Reactter.on(appController, Lifecycle.didUpdate, onDidUpdate);
Reactter.emit(appController, Lifecycle.didUpdate, 'test param');
RECOMMENDED: Use the instance object directly on event methods for optimal performance.
NOTE: The
ReactterInstancehelps to find the instance for event, if the instance not exists, put it on wait. It's a good option if you're not sure that the instance has been created yet.
UseEffect #
UseEffect is a hook(ReactterHook) that allows to manage side-effect.
UseEffect(
<Function cleanup> Function callback,
List<ReactterState> dependencies,
[Object? instance]
)
The side-effect logic into the callback function is executed when the dependencies argument changes or the instance trigger Lifecycle.didMount event.
If the callback returns a function, then UseEffect considers this as an effect cleanup.
The cleanup callback is executed, before callback is called or instance trigger Lifecycle.willUnmount event:
Let's see an example with a counter that increments every second:
class AppController {
final count = UseState(0);
AppController() {
UseEffect((){
// Execute by count state changed or 'Lifecycle.didMount' event
print("Count: ${count.value}");
Future.delayed(const Duration(seconds: 1), () => count.value += 1);
return () {
// Cleanup - Execute before count state changed or 'Lifecycle.willUnmount' event
print("Cleanup executed");
};
}, [count], this);
}
}
Use UseEffect.dispatchEffect instead of instance argument to execute a UseEffect immediately.
UseEffect(
() => print("Excute immediately or by hook changes"),
[someState],
UseEffect.dispatchEffect
);
NOTE: If you don't add an
instanceargument toUseEffect, thecallbackwon't execute onLifecycle.didMount, and thecleanupwon't execute onLifecycle.willUnmount(thesesLifecycleevents are used withflutter_reactteronly).
ReactterConsumer #
ReactterConsumer is a Widget (exclusive of flutter_reactter) that allows to access the instance of T type from ReactterProvider's nearest ancestor and can listen all or specified states to rebuild the Widget when theses changes occur:
ReactterConsumer<T>({
String? id,
bool listenAll = false,
List<ReactterState> listenStates(T instance)?,
Widget? child,
required Widget builder(T instance, BuildContext context, Widget? child),
});
ReactterConsumer accepts theses properties:
id: to uniquely identify the instance.listenAll: to listen to all events emitted by the instance or its states(ReactterState).listenStates: to listen to states(ReactterState) defined in it.child: to pass aWidgetthrough thebuildermethod that it will be built only once.builder: to define a method that contains the builder logic of the widget that will be embedded in the widget tree. This method exposes theinstance(T) created, a newcontext(BuildContext) and achild(Widget) defined in thechildproperty.
Here an example:
class ExampleWidget extends StatelessWidget {
...
Widget build(context) {
return ReactterConsumer<AppController>(
listenStates: (inst) => [inst.stateA, inst.stateB],
child: const Text('This widget is rendered once'),
builder: (appController, context, child) {
// This is built when stateA or stateB has changed.
return Column(
children: [
Text("My instance: $appContoller"),
Text("StateA: ${appContoller.stateA.value}"),
Text("StateB: ${appContoller.stateB.value}"),
child!, // The child widget has already been built in `child` property.
],
);
}
);
}
}
NOTE:
ReactteConsumeris "scoped". So, thebuildermethod will be rebuild when the instance or anyReactterStatespecified the watch methods of BuildContext extension changes.
NOTE: Use
List<ReactterState>.whenextension for more specific conditional state when you want the widget tree to be re-rendered. For example:class ExampleWidget extends StatelessWidget { ... Widget build(context) { return ReactterConsumer<AppController>( listenStates: (inst) => [inst.stateA, inst.stateB].when( () => inst.stateA.value == inst.stateB.value, // condition // The following condition functions as `or` like: // condition || condition2 || condition3 () => inst.stateA.value == 'X', // condition2 () => inst.stateB.value == 'Y', // condition3 ), builder: (appController, context, child) { // This is rebuilt according to the above conditions. return Column( children: [ Text("My instance: $appContoller"), Text("StateA: ${appContoller.stateA.value}"), Text("StateB: ${appContoller.stateB.value}"), ], ); } ); } }
ReactterWatcher #
ReactterWatcher is a Widget (exclusive of flutter_reactter) that allows to listen all Signals contained in builder property and rebuilt the Widget when it changes:
ReactterWatcher({
Widget? child,
required Widget builder(BuildContext context, Widget? child),
})
ReactterWatcher accepts two properties:
child: to pass aWidgetthrough thebuildermethod that it will be built only once.builder: to define a method that contains the builder logic of the widget that will be embedded in the widget tree. This method exposes a newcontext(BuildContext) and achild(Widget) defined in thechildproperty.
final count = 0.signal;
final flag = false.signal;
void increase() => count.value += 1;
void toggle() => flag(!flag.value);
class App extends StatelessWidget {
...
Widget build(context) {
return ReactterWatcher(
// This widget is rendered once only and passed through the `builder` method.
child: Row(
children: const [
ElevatedButton(
onPressed: increase,
child: Text("Increase"),
),
ElevatedButton(
onPressed: toggle,
child: Text("Toogle"),
),
],
),
builder: (context, child) {
// Rebuilds the Widget tree returned when `count` or `flag` are updated.
return Column(
children: [
Text("Count: $count"),
Text("Flag is: $flag"),
child!, // Takes the Widget from the `child` property in each rebuild.
],
);
},
);
}
}
BuildContext extension #
Reactter provides additional methods through BuildContext to access to instance. These are following:
context.watch: Gets the instance ofTfromReactterProvider's nearest ancestor and listens any instance changes orReactterStatechanges declared in first paramater.
// listens any `AppController` changes
final appController = context.watch<AppController>();
// listens the states changes declared in first paramater.
final appController = context.watch<AppController>(
(inst) => [inst.stateA, inst.stateB],
);
NOTE: You can use
List<ReactterState>.whenextension for more specific conditional state when you want the widget tree to be re-rendered. For example:
final appController = context.watch<AppController>(
(inst) => [inst.stateA, inst.stateB].when(
() => inst.stateA.value == inst.stateB.value, // condition
// The following condition functions as `or` like:
// condition || condition2 || condition3
() => inst.stateA.value == 'X', // condition2
() => inst.stateB.value == 'Y', // condition3
),
);
context.watchId: Gets the instance ofTtype byidfromReactterProvider's nearest ancestor and listens instance changes orReactterStatechanges declared in second paramater.
// listens any `TestController` by `id` changes
final testController = context.watchId<TestController>('UniqueId');
// listens the states changes declared in second paramater.
final testController = context.watchId<TestController>(
'UniqueId',
(inst) => [inst.stateA, inst.stateB],
);
context.use: Gets the instance ofTtype with/withoutidfromReactterProvider's nearest ancestor.
final testController = context.use<TestController>();
final testControllerWithId = context.use<TestController>('UniqueId');
NOTE: These methods mentioned above uses
ReactterProvider.contextOf.
NOTE:
context.watchandcontext.watchIdwatch all or some of the specifiedReactterStatedependencies, when any it will changes, re-built the Widgets tree in the scope ofReactterProvider,ReactterComponentor any Widget that exposes theBuildContextlikeBuild,StatelessWidget,StatefulWidget.
NOTE: A
ReactterStatecan be aSignalorReactterHook(likeUseState,UseAsynState,UseReducer,UseComputeor another Custom hooks).
Custom hooks #
Custom hooks are classes that extend ReactterHook that follow a special naming convention with the use prefix and can contain state logic, effects or any other custom code.
There are several advantages to using Custom Hooks:
- Reusability: you can use the same hook again and again, without the need to write it twice.
- Clean Code: extracting part of context logic into a hook will provide a cleaner codebase.
- Maintainability: easier to maintain. if you need to change the logic of the hook, you only need to change it once.
Here's the counter example:
class UseCount extends ReactterHook {
final $ = ReactterHook.$register;
int _count = 0;
int get value => _count;
UseCount(int initial) : _count = initial;
void increment() => update(() => _count += 1);
void decrement() => update(() => _count -= 1);
}
IMPORTANT: To create a
ReactterHook, you need to register it by adding the following line:final $ = ReactterHook.$register;
NOTE:
ReactterHookprovides anupdatemethod which notifies about its changes.
You can then call that custom hook from anywhere in the code and get access to its shared logic:
class AppController {
final count = UseCount(0);
AppController() {
Timer.periodic(Duration(seconds: 1), (_) => count.increment());
// Print count value every second
Reactter.on(
count,
Lifecycle.didUpdate,
(_, __) => print("Count: ${count.value}",
);
}
}
Lazy state #
A lazy state is a ReactterState(like a Signal or ReactterHook) that is loaded lazily using Reactter.lazyState.
T Reactter.lazyState<T extends ReactterState>(T stateFn(), Object instance);
Reactter.lazyState is generally used in states declared with the late keyword.
In dart,
latekeyword is used to declare a variable or field that will be initialized at a later time. It is used to declare a non-nullable variable that is not initialized at the time of declaration.
For example, when the a state declared in a class requires some variable or methods immediately:
class AppController {
final String initialValue = 'test';
dynamic resolveValue() async => [...];
/// late final state = UseAsyncState(
/// initialValue,
/// resolveValue
/// ); <- to use `Reactter.lazyState` is required, like:
late final state = Reactter.lazyState(
() => UseAsyncState(initialValue, resolveValue),
this,
);
...
}
IMPORTANT: A state(
ReactterState) declared with thelatekeyword and not usingReactter.lazyStateis outside the context of the instance where it was declared, and therefore the instance does not notice about its changes.
Generic arguments #
Generic arguments are objects of the Args class that represent the arguments of the specified types.
It is used to define the arguments that are passed through a Function and allows to type the Function appropriately.
RECOMMENDED: If your project supports
Record, it is recommended to use it instead of the generic arguments.
Reactter provides theses generic arguments classes:
Args<A>: Represents one or more arguments ofAtype.Args1<A>: Represents a argument ofAtype.Args2<A, A2>: Represents two arguments ofA,A2type consecutively.Args3<A, A2, A3>: Represents three arguments ofA,A2,A3type consecutively.ArgsX2<A>: Represents two arguments ofAtype.ArgsX3<A>: Represents three arguments ofAtype.
In each of the methods it provides theses methods and properties:
arguments- gets the list of arguments.toList<T>()- gets the list of argumentsTtype.arg1- gets to the first argument.arg2(Args2,Args3,ArgsX2,ArgsX3only) - gets to the second argument.arg3(Args3,ArgsX3only) - gets to the third argument.
NOTE: If you need a generic argument class with more arguments, then create a new class following the pattern:
class Args+n<A, (...), A+n> extends Args+(n-1)<A, (...), A+(n-1)> { final A+n arg+n; const Args+n(A arg1, (...), A+(n-1) arg+(n-1), this.arg+n) : super(arg1, (...), arg+(n-1)); @override List get arguments => [...super.arguments, arg+n]; } typedef ArgX+n<T> = Args+n<T, (...), T>;e.g 4 arguments:
class Args4<A, A2, A3, A4> extends Args3<A, A2, A3> { final A4 arg4; const Args4(A arg1, A2 arg2, A3 arg3, this.arg4) : super(arg1, arg2, arg3); @override List get arguments => [...super.arguments, arg4]; } typedef ArgX4<T> = Args4<T, T, T, T>;
NOTE: Use
aryFunction extention to convert anyFunctionwith positional arguments toFunctionwith generic argument, e.g:int addNum(int num1, int num2) => num1 + num2; // convert to `int Function(Args2(int, int))` final addNumAry = myFunction.ary; addNumAry(Arg2(1, 1)); // or call directly addNum.ary(ArgX2(2, 2));
Memo #
Memo is a class callable with memoization logic which it stores computation results in cacbe, and retrieve that same information from the cache the next time it's needed instead of computing it again.
NOTE: Memoization is a powerful trick that can help speed up our code, especially when dealing with repetitive and heavy computing functions.
Memo<T, A>(
T computeValue(A arg), [
MemoInterceptor<T, A>? interceptor,
]);
Memo accepts theses properties:
computeValue: Represents a function that takes an argument of typeAand returns a value of typeT. This is the core function that will be memoized.interceptor: Receives aMemoInterceptorthat allows you to intercept the memoization function calls and modify the memoization process. Reactter providers some interceptors:MemoInterceptors: Allows multiple memoization interceptors to be used together.MemoInterceptorWrapper: A wrapper for a memoized function that allows you to define callbacks for initialization, successful completion, error handling, and finishing.AsyncMemoSafe: Prevents saving in cache if theFuturecalculation function throws an error when executed.TemporaryCacheMemo: Removes memoized values from the cache after a specified duration.
Here an factorial example using Memo:
late final factorialMemo = Memo(calculateFactorial);
/// A factorial(n!) represents the multiplication of all numbers between 1 and n.
/// So if you were to have 3!, for example, you'd compute 3 x 2 x 1 (which = 6).
BigInt calculateFactorial(Arg<int> args) {
final numero = args.arg;
if (numero == 0) return BigInt.one;
return BigInt.from(numero) * factorialMemo(Arg(numero - 1));
}
void main() {
// Returns the result of multiplication of 1 to 50.
final f50 = factorialMemo(const Arg(50));
// Returns the result immediately from cache
// because it was resolved in the previous line.
final f10 = factorialMemo(const Arg(10));
// Returns the result of the multiplication of 51 to 100
// and 50! which is obtained from the cache.
final f100 = factorialMemo(const Arg(100));
print(
'Results:\n'
'\t10!: $f10\n'
'\t50!: $f50\n'
'\t100!: $f100\n'
);
}
NOTE: Use
Memo.inlinein case there is a typing conflict, e.g. with theUseAsynStateandUseComputehooks which aFunctiontype is required.
Memo provides the following methods that will help you manipulate the cache as you wish:
T? get(A arg): Returns the cached value byarg.T? remove(A arg): Removes the cached value byarg.clear: Removes all cached data.
Difference between Signal and UseState #
Both UseState and Signal represent a state (ReactterState). However, it possesses distinct features that set them apart.
UseState is a ReactterHook, giving it the unique ability to be extended and enriched with new capabilities, which sets it apart from Signal.
In the case of UseState, it necessitates the use of the value attribute whenever state is read or modified. On the other hand, Signal streamlines this process, eliminating the need for explicit value handling, thus enhancing code clarity and ease of understanding.
In the context of Flutter, when implementing UseState, it is necessary to expose the parent class containing the state to the widget tree via a ReactterProvider or ReactterComponent, and subsequently access it through BuildContext. Conversely, with Signal, which is inherently reactive, you can conveniently employ ReactterWatcher.
It's important to note that while Signal offers distinct advantages, particularly for managing global states and enhancing code readability, it can introduce potential antipatterns and may complicate the debugging process. Nevertheless, these concerns are actively being addressed and improved in upcoming versions of the package.
Ultimately, the choice between UseState and Signal lies in your hands. They can coexist seamlessly, and you have the flexibility to transition from UseState to Signal, or vice versa, as your project's requirements evolve.
Resources #
- Github
- Examples
- Examples in zapp
- Reactter doccumentation
- Flutter Reactter documentation
- Reactter Lint
- Reactter Snippets
Contribute #
If you want to contribute don't hesitate to create an issue or pull-request in Reactter repository.
You can:
- Provide new features.
- Report bugs.
- Report situations difficult to implement.
- Report an unclear error.
- Report unclear documentation.
- Add a new custom hook.
- Add a new widget.
- Add examples.
- Translate documentation.
- Write articles or make videos teaching how to use Reactter.
Any idean is welcome!