A light, powerful and quick Reactive State Management, Dependency Injection and Event Handler.
Now documentation on the official web site: 2devs-team.github.io/reactter
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 and Hooks.
- ♻️ Reusable States and Logic with Custom hooks.
- 🎮 Fully Rendering Control.
- 🧪 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 = Signal(0);
void main() {
// Change the `value` in any time(e.g., each 1 second).
Timer.periodic(
Duration(seconds: 1),
(timer) => count.value++,
);
// Put on listen `didUpdate` event, whitout use `Stream`
Rt.on(
count,
Lifecycle.didUpdate,
(inst, state) => print('Count: $count'),
);
// And you can use in flutter, e.g:
runApp(
MaterialApp(
home: Scaffold(
body: RtSignalWatcher(
// Just use it, and puts it in listening mode
// for further rendering automatically.
builder: (context, child) => Text("Count: $count"),
),
),
),
);
}
Clean and easy!
See more examples here!
Contents
- Features
- Contents
- Quickstart
- About Reactter
- State management
- Dependency injection
- Event handler
- Rendering control
- Custom hooks
- Lazy state
- Batch
- Untracked
- 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.yaml
file:dependencies: reactter: #add version here
and 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.yaml
file:dependencies: flutter_reactter: #add version here
and 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:
Moreover, Reactter offers an extensive collection of widgets and extensions, granting advanced rendering control through the flutter_reactter
package.
State management
In Reactter, state is understood as any object that extends RtState
, 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
RtHook
) are named with the prefixUse
according to convention.
RECOMMENDED: See also difference between Signal and UseState and about custom hooks.
Signal
Signal
is an object (that extends RtState
) which has a value
and notifies about its changes.
It can be initialized 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:
Signal
notifies that itsvalue
has changed when the previousvalue
is different from the currentvalue
. If itsvalue
is anObject
, it does not detect internal changes, only whenvalue
is 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.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecycle.didUpdate
event is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
NOTE: When you do any arithmetic operation between two
Signal
s, it returns anObj
, for example:signal(1) + Signal(2)
returnsObj(3)
. AnObj
is like aSignal
without reactive functionality, but you can convert it toSignal
using.toSignal
.
NOTE: In flutter, using
RtSignalWatcher
, is a way to keep the widgets automatically updates, accessing the value of signal reactively.
UseState
UseState
is a hook(RtHook
) 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
late
useRt.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:
UseState
notifies that itsvalue
has changed when the previousvalue
is different from the currentvalue
. If itsvalue
is anObject
, it does not detect internal changes, only whenvalue
is 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.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecycle.didUpdate
event is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
UseAsyncState
UseAsyncState
is a hook (RtHook
) 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 these 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 theresolved
method 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
late
useRt.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.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecycle.didUpdate
event is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
UseReducer
UseReducer
is a hook(RtHook
) that manages state using reducer method. An alternative to UseState
.
RECOMMENDED:
UseReducer
is usually preferable overUseState
when 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, RtAction<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 RtAction
.
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, RtAction<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(RtAction(type: 'INCREMENT', payload: 2));
print("count: ${useCounter.value.count}"); // count: 2;
useCounter.dispatch(RtAction(type: 'DECREMENT'));
print("count: ${useCounter.value.count}"); // count: 1;
}
}
The actions can be created as a callable class, extending from RtActionCallable
and used as follows:
class IncrementAction extends RtActionCallable<Store, int> {
IncrementAction([int quantity = 1]) : super(
type: 'INCREEMNT', payload: quantity
);
@override
Store call(Store state) => Store(count: state.count + payload);
}
class DecrementAction extends RtActionCallable<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, RtAction action) {
if (action is RtActionCallable) 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.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecycle.didUpdate
event is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
UseCompute
UseCompute
is a hook(RtHook
) that keeps listening for state dependencies
changes, to return a computed value(T
) from a defined method(computeValue
).
UseCompute<T>(
T computeValue(),
List<RtState> dependencies,
)
UseCompute
accepts two arguments:
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 thatUseCompute
keeps an active watch on, listening for any changes that may occur for calling thecomputeValue
function.
so, here is an example:
class MyController {
final stateA = UseState(1);
final stateB = UseState(7);
late final computeState = Rt.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}");
MyController() {
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:
UseCompute
notifies that itsvalue
has changed when the previousvalue
is different from the currentvalue
.
When value
has changed, the UseState
will emit the following events (learn about it here):
Lifecycle.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecycle.didUpdate
event is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
NOTE:
UseCompute
is read-only, meaning that its value cannot be changed, except by invoking thecomputeValue
method.
RECOMENDED:
UseCompute
does not cache the computed value, meaning it recalculates the value when its depenencies has changes, potentially impacting performance, especially if the computation is expensive. In these cases, you should consider usingMemo
(learn about it here) in the following manner:
late final myUseComputeMemo = Rt.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 DependencyMode.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 DependencyMode.factory
and to active it, set it in the mode
argument of Rt.register
and Rt.create
, or use Rt.lazyFactory
, Rt.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 an 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 DependencyMode.singleton
and to active it, set it in the mode
argument of Rt.register
and Rt.create
, or use Rt.lazySingleton
, Rt.singleton
.
NOTE: Use
Rt.destroy
if 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:
Rt.register
: Registers a builder function, for creating a new instance using[Rt|UseDependency].[get|create|builder|factory|singleton]
.Rt.lazyBuilder
: Registers a builder function, for creating a new instance as Builder mode using[Rt|UseDependency].[get|create|builder]
.Rt.lazyFactory
: Registers a builder function, for creating a new instance as Factory mode using[Rt|UseDependency].[get|create|factory]
.Rt.lazySingleton
: Registers a builder function, for creating a new instance as Singleton mode using[Rt|UseDependency].[get|create|singleton]
.Rt.create
: Registers, creates and returns the instance directly.Rt.builder
: Registers, creates and returns the instance as Builder directly.Rt.factory
: Registers, creates and returns the instance as Factory directly.Rt.singleton
: Registers, creates and returns the instance as Singleton directly.Rt.get
: Returns a previously created instance or creates a new instance from the builder function registered by[Rt|UseDependency].[register|lazyBuilder|lazyFactory|lazySingleton]
.Rt.delete
: Deletes the instance but keeps the builder function.Rt.unregister
: Removes the builder function, preventing the creation of the instance.Rt.destroy
: Destroys the instance and the builder function.Rt.find
: Gets the instance.Rt.isRegistered
: Checks if an instance is registered in Reactter.Rt.getDependencyMode
: Returns theDependencyMode
of the instance.
In each of the events methods shown above (except Rt.isRegister
and Rt.getDependencyMode
), 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
UseDependency
will allow you to access them from anywhere in the project.
UseDependency
UseDependency
is a hook(RtHook
) that allows to manage an instance.
UseDependency<T>([String? id]);
The default constructor uses Rt.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 injection
before.
Use instance
getter to get the instance.
Here is an example using UseIntance
:
class MyController {
final useAuthController = UseDependency<AuthController>();
// final useOtherControllerWithId = UseDependency<OtherController>("UniqueId");
AuthController? authController = useAuthController.instance;
MyController() {
UseEffect(() {
authController = useAuthController.instance;
}, [useAuthController],
);
}
}
NOTE: In the example above uses
UseEffect
hook, to wait for theinstance
to become available.
UseDependency
provides some constructors and factories for managing an instance, which are:
UseDependency.register
: Registers a builder function, for creating a new instance using[Rt|UseDependency].[get|create|builder|factory|singleton]
.UseDependency.lazyBuilder
: Registers a builder function, for creating a new instance as Builder mode using[Rt|UseDependency].[get|create|builder]
.UseDependency.lazyFactory
: Registers a builder function, for creating a new instance as Factory mode using[Rt|UseDependency].[get|create|factory]
.UseDependency.lazySingleton
: Registers a builder function, for creating a new instance as Singleton mode using[Rt|UseDependency].[get|create|singleton]
.UseDependency.create
: Registers, creates and returns the instance directly.UseDependency.builder
: Registers, creates and returns the instance as Builder directly.UseDependency.factory
: Registers, creates and returns the instance as Factory directly.UseDependency.singleton
: Registers, creates and returns the instance as Singleton directly.UseDependency.get
: Returns a previously created instance or creates a new instance from the builder function registered by[Rt|UseDependency].[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
UseDependency
will allow you to access them from anywhere in the project.
Event handler
In Reactter, event handler plays a pivotal role in facilitating seamless communication and coordination between various components within the application. The event handler system is designed to ensure efficient handling of states and instances, fostering a cohesive ecosystem where different parts of the application can interact harmoniously.
One of the key aspects of event handler in Reactter is the introduction of lifecycles linked to events. These lifecycles define the different stages through which a state or instance passes, offering a structured flow and effective handling of changes.
Additionally, Reactter offers the following event managers:
by flutter_reactter
:
Lifecycles
In Reactter, both the states (RtState
) and the instances (managed by the dependency injection
) contain different stages, also known as Lifecycle
.
This lifecycles linked events, which are:
Lifecycle.registered
: is triggered when the dependency has been registered.Lifecycle.created
: is triggered when the dependency instance has been created.Lifecycle.willMount
(exclusive offlutter_reactter
): is triggered when the dependency is going to be mounted in the widget tree.Lifecycle.didMount
(exclusive offlutter_reactter
): is triggered after the dependency has been successfully mounted in the widget tree.Lifecycle.willUpdate
: is triggered anytime the dependency's state is about to be updated. The event parameter is aRtState
.Lifecycle.didUpdate
: is triggered anytime the dependency's state has been updated. The event parameter is aRtState
.Lifecycle.willUnmount
(exclusive offlutter_reactter
): is triggered when the dependency is about to be unmounted from the widget tree.Lifecycle.didUnmount
(exclusive offlutter_reactter
): is triggered when the dependency has been successfully unmounted from the widget tree.Lifecycle.deleted
: is triggered when the dependency instance has been deleted.Lifecycle.unregistered
: is triggered when the dependency is no longer registered.
You can extend your instances with LifecycleObserver
mixin for observing and reacting to the various lifecycle events. e.g:
class MyController with LifecycleObserver {
final state = UseState('initial');
@override
void onInitialized() {
print("MyController has been initialized");
}
@override
void onDidUpdate(RtState? state) {
print("$state has been changed");
}
}
final myController = Rt.create(() => MyController());
// MyController has been initialized
myController.state.value = "value changed";
// state has been changed
Shortcuts to manage events
Reactter offers several convenient shortcuts for managing events:
-
Rt.on
: turns on the listen event. When theevent
ofinstance
is emitted, thecallback
is called:Rt.on<T, P>(Object instance, Enum event, callback(T inst, P params));
-
Rt.one
: turns on the listen event for only once. When theevent
ofinstance
is emitted, thecallback
is called and then removed.Rt.one<T, P>(Object instance, Enum event, callback(T inst, P param));
-
Rt.off
: removes thecallback
fromevent
ofinstance
.Rt.off<T, P>(Object instance, Enum event, callback(T instance, P param));
-
Rt.offAll
: removes all events ofinstance
.Rt.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
Rt.off
to remove the specific events. -
Rt.emit
: triggers anevent
ofinstance
with or without theparam
given.Rt.emit(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 RtInstance
instead:
void onDidUpdate(inst, state) => print("Instance: $inst, state: $state");
final myController = Rt.get<MyController>();
// or using `RtDependency`
final myController = RtDependency<MyController>();
Rt.on(myController, Lifecycle.didUpdate, onDidUpdate);
Rt.emit(myController, Lifecycle.didUpdate, 'test param');
RECOMMENDED: Use the instance object directly on event methods for optimal performance.
NOTE: The
RtDependency
helps 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(RtHook
) that allows to manage side-effect.
UseEffect(
<Function cleanup> Function() callback,
List<RtState> dependencies,
)
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 MyController {
final count = UseState(0);
MyController() {
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]);
}
}
Use UseEffect.runOnInit
to execute the callback effect on initialization.
UseEffect.runOnInit(
() => print("Excute immediately and by hook changes"),
[someState],
);
Rendering control
Rendering control provides the capability to observe specific instances or states, triggering re-renders of the widget tree as required. This methodology ensures a unified and responsive user interface, facilitating efficient updates based on changes in the application's state.
In this context, the flutter_reactter
package provides the following purpose-built widgets and certain BuildContext
extension for rendering control:
- RtProvider
- RtProviders
- RtComponent
- RtConsumer
- RtSignalWatcher
- RtSelector
- BuildContext.use
- BuildContext.watch
- BuildContext.select
RtProvider
RtProvider
is a Widget (exclusive of flutter_reactter
) that hydrates from an instance of T
type to the Widget tree.
RtProvider<T>(
T instanceBuilder(), {
String? id,
DependencyMode type = DependencyMode.builder,
Widget? child,
required Widget builder(BuilderContext context, T instance, Widget? child),
})
RtProvider
accepts these properties:
-
instanceBuilder
: to define a method for the creation of a new instance ofT
type.RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
-
id
: to uniquely identify the instance. -
mode
: to determine the instance manage mode(Builder, Factory or Singleton). -
child
: to pass aWidget
through thebuilder
method 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 thechild
property.
Here is an example:
RtProvider<CounterController>(
() => CounterController(),
child: const Text('This widget is rendered once'),
builder: (context, counterController, child) {
// `context.watch` listens any CounterController changes for rebuild this widget tree.
context.watch<CounterController>();
// Change the `value` each 1 second.
Future.delayed(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}"),
],
);
},
)
Use RtProvider.init
to initialize the dependency instance before that it's mounted.
Use RtProvider.lazy
to enable lazy-loading of the instance, ensuring it is only instantiated when necessary. While this feature enhances performance by deferring instantiation until required, it's important to note that it may result in the loss of lifecycle tracing.
NOTE:
RtProvider
is "scoped". So, thebuilder
method will be rebuild when the instance or anyRtState
specified inBuildContext.watch
orBuildContext.select
changes.
RtMultiProvider
RtMultiProvider
is a Widget (exclusive of flutter_reactter
) that allows to use multiple RtProvider
in a nested way.
RtMultiProvider(
[
RtProvider(
() => MyController(),
),
RtProvider(
() => ConfigController(),
id: 'App',
),
RtProvider(
() => ConfigController(),
id: 'Dashboard'
),
],
builder: (context, child) {
final myController = context.use<MyController>();
final appConfigController = context.use<ConfigController>('App');
final dashboardConfigController = context.use<ConfigController>('Dashboard');
...
},
)
RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
NOTE:
RtProvider
is "scoped". So, thebuilder
method will be rebuild when the instance or anyRtState
specified inBuildContext.watch
orBuildContext.select
changes.
RtComponent
RtComponent
is a abstract StatelessWidget
(exclusive of flutter_reactter
) that provides RtProvider
features, whose instance of T
type is exposed trough render
method.
class CounterComponent extends RtComponent<CounterController> {
const CounterComponent({Key? key}) : super(key: key);
@override
get builder => () => CounterController();
@override
void listenStates(counterController) => [counterController.count];
@override
Widget render(context, counterController) {
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
builder
getter, 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.
RtConsumer
RtConsumer
is a Widget (exclusive of flutter_reactter
) that allows to access the instance of T
type from RtProvider
's nearest ancestor and can listen all or specified states to rebuild the Widget when these changes occur:
RtConsumer<T>({
String? id,
bool listenAll = false,
List<RtState> listenStates(T instance)?,
Widget? child,
required Widget builder(BuildContext context, T instance, Widget? child),
});
RtConsumer
accepts these properties:
id
: to uniquely identify the instance.listenAll
: to listen to all events emitted by the instance or its states(RtState
).listenStates
: to listen to states(RtState
) defined in it.child
: to pass aWidget
through thebuilder
method 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 thechild
property.
Here is an example:
class ExampleWidget extends StatelessWidget {
...
Widget build(context) {
return RtConsumer<MyController>(
listenStates: (inst) => [inst.stateA, inst.stateB],
child: const Text('This widget is rendered once'),
builder: (context, myController, child) {
// This is built when stateA or stateB has changed.
return Column(
children: [
Text("My instance: $d"),
Text("StateA: ${d.stateA.value}"),
Text("StateB: ${d.stateB.value}"),
child!, // The child widget has already been built in `child` property.
],
);
}
);
}
}
NOTE:
ReactteConsumer
is "scoped". So, thebuilder
method will be rebuild when the instance or anyRtState
specified get change.
NOTE: Use
RtSelector
for more specific conditional state when you want the widget tree to be re-rendered.
RtSelector
RtSelector
is a Widget (exclusive of flutter_reactter
) that allows to control the rebuilding of widget tree by selecting the states(RtState
) and a computed value.
RtSelector<T, V>(
V selector(
T inst,
RtState $(RtState state),
),
String? id,
Widget? child,
Widget builder(
BuildContext context,
T inst,
V value,
Widget? child
),
)
RtSelector
accepts four properties:
selector
: to define a method that contains the computed value logic and determined when to be rebuilding the widget tree which defined inbuild
property. It returns a value ofV
type and exposes the following arguments:inst
: the found instance ofT
type and byid
if specified it.$
: a method that allows to wrap to the state(RtState
) to put it in listening.
id
: to uniquely identify the instance.child
: to pass aWidget
through thebuilder
method 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. It exposes the following arguments:context
: a newBuilContext
.inst
: the found instance ofT
type and byid
if specified it.value
: the computed value ofV
type. It is computed byselector
method.child
: aWidget
defined in thechild
property.
RtSelector
determines if the widget tree of builder
needs to be rebuild again by comparing the previous and new result of selector
.
This evaluation only occurs if one of the selected states(RtState
) gets updated, or by the instance if the selector
does not have any selected states(RtState
). e.g.:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
return RtProvider<MyController>(
() => MyController(),
builder: (context, inst, child) {
return OtherWidget();
}
);
}
}
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
return RtSelector<MyController, int>(
selector: (inst, $) => $(inst.stateA).value % $(inst.stateB).value,
builder: (context, inst, value, child) {
// This is rebuilt every time that the result of selector is different to previous result.
return Text("${inst.stateA.value} mod ${inst.stateB.value}: ${value}");
},
);
}
}
RtSelector
typing can be ommited, but the app must be wrapper by RtScope
. e.g.:
[...]
RtScope(
child: MyApp(),
)
[...]
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
final myController = context.use<MyController>();
return RtSelector(
selector: (_, $) => $(myController.stateA).value % $(myController.stateB).value,
builder: (context, _, value, child) {
// This is rebuilt every time that the result of selector is different to previous result.
return Text("${myController.stateA.value} mod ${myController.stateB.value}: ${value}");
},
);
}
RtSignalWatcher
RtSignalWatcher
is a Widget (exclusive of flutter_reactter
) that allows to listen all Signal
s contained in builder
property and rebuilt the Widget when it changes:
RtSignalWatcher({
Widget? child,
required Widget builder(BuildContext context, Widget? child),
})
RtSignalWatcher
accepts two properties:
child
: to pass aWidget
through thebuilder
method 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. It exposes the following arguments:context
: a newBuilContext
.child
: aWidget
defined in thechild
property.
final count = Signal(0);
final flag = Signal(false);
void increase() => count.value += 1;
void toggle() => flag(!flag.value);
class App extends StatelessWidget {
...
Widget build(context) {
return RtSignalWatcher(
// 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.use
BuildContext.use
is an extension method of the BuildContext
, that allows to access to instance of T
type from the closest ancestor RtProvider
.
T context.use<T>([String? id])
Here is an example:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
return RtProvider<MyController>(
() => MyController(),
builder: (context, inst, child) {
return OtherWidget();
}
);
}
}
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
final myController = context.use<MyController>();
return Text("value: ${myController.stateA.value}");
}
}
Use the first argument for obtaining the instance by id
. e.g.:
final myControllerById = context.use<MyController>('uniqueId');
Use the nullable type to safely get the instance, avoiding exceptions if the instance is not found, and get null
instead. e.g.:
final myController = context.use<MyController?>();
NOTE: If
T
is non-nullable and the instance is not found, it will throwProviderNullException
.
BuildContext.watch
BuildContext.watch
is an extension method of the BuildContext
, that allows to access to instance of T
type from the closest ancestor RtProvider
, and listen to the instance or RtState
list for rebuilding the widget tree in the scope of BuildContext
.
T context.watch<T>(
List<RtState> listenStates(T inst)?,
)
Here is an example, that shows how to listen an instance and react for rebuild:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
return RtProvider<MyController>(
() => MyController(),
builder: (context, inst, child) {
return OtherWidget();
}
);
}
}
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
final myController = context.watch<MyController>();
// This is rebuilt every time any states in the instance are updated
return Text("value: ${myController.stateA.value}");
}
}
Use the first argument(listenStates
) to specify the states that are to be listen on for rebuild. e.g.:
[...]
@override
Widget? build(BuildContext context) {
final myController = context.watch<MyController>(
(inst) => [inst.stateA, inst.stateB],
);
// This is rebuilt every time any defined states are updated
return Text("value: ${myController.stateA.value}");
}
[...]
Use BuildContext.watchId
for obtaining the instance of T
type by id
, and listens the instance or RtState
list for rebuilding the widget tree in the scope of BuildContext
.
T context.watchId<T>(
String id,
List<RtState> listenStates(T inst)?,
)
It is used as follows:
// for listening the instance
final myControllerById = context.watchId<MyController>('uniqueId');
// for listening the states
final myControllerById = context.watchId<MyController>(
'uniqueId',
(inst) => [inst.stateA, inst.stateB],
);
BuildContext.select
BuildContext.select
is an extension method of the BuildContext
, that allows to control the rebuilding of widget tree by selecting the states(RtState
) and a computed value.
V context.select<T, V>(
V selector(
T inst,
RtState $(RtState state),
),
[String? id],
)
BuildContext.select
accepts two argtuments:
selector
: to define a method that computed value logic and determined when to be rebuilding the widget tree of theBuildContext
. It returns a value ofV
type and exposes the following arguments:inst
: the found instance ofT
type and byid
if specified it.$
: a method that allows to wrap to the state(RtState
) to put it in listening.
id
: to uniquely identify the instance.
BuildContext.select
determines if the widget tree in scope of BuildContext
needs to be rebuild again by comparing the previous and new result of selector
.
This evaluation only occurs if one of the selected states(RtState
) gets updated, or by the instance if the selector
does not have any selected states(RtState
). e.g.:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
return RtProvider<MyController>(
() => MyController(),
builder: (context, inst, child) {
return OtherWidget();
}
);
}
}
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
final value = context.select<MyController, int>(
(inst, $) => $(inst.stateA).value % $(inst.stateB).value,
);
// This is rebuilt every time that the result of selector is different to previous result.
return Text("stateA mod stateB: ${value}");
}
}
BuildContext.select
typing can be ommited, but the app must be wrapper by RtScope
. e.g.:
[...]
RtScope(
child: MyApp(),
)
[...]
class OtherWidget extends StatelessWidget {
const OtherWidget({Key? key}) : super(key: key);
@override
Widget? build(BuildContext context) {
final myController = context.use<MyController>();
final value = context.select(
(_, $) => $(myController.stateA).value % $(myController.stateB).value,
);
// This is rebuilt every time that the result of selector is different to previous result.
return Text("stateA mod stateB: ${value}");
}
}
Custom hooks
Custom hooks are classes that extend RtHook
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: to use the same hook again and again, without the need to write it twice.
- Clean Code: extracting part of code 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 RtHook {
final $ = RtHook.$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
RtHook
, you need to register it by adding the following line:final $ = RtHook.$register;
NOTE:
RtHook
provides anupdate
method 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 MyController {
final count = UseCount(0);
MyController() {
Timer.periodic(Duration(seconds: 1), (_) => count.increment());
// Print count value every second
Rt.on(
count,
Lifecycle.didUpdate,
(_, __) => print("Count: ${count.value}",
);
}
}
Lazy state
A lazy state is a RtState
(Signal
or RtHook
) that is loaded lazily using Rt.lazyState
.
T Rt.lazyState<T extends RtState>(T stateFn(), Object instance);
Rt.lazyState
is generally used in states declared with the late
keyword.
In dart,
late
keyword 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 MyController {
final String initialValue = 'test';
dynamic resolveValue() async => [...];
/// late final state = UseAsyncState(
/// initialValue,
/// resolveValue
/// ); <- to use `Rt.lazyState` is required, like:
late final state = Rt.lazyState(
() => UseAsyncState(initialValue, resolveValue),
this,
);
...
}
IMPORTANT: A state(
RtState
) declared with thelate
keyword and not usingRt.lazyState
is outside the context of the instance where it was declared, and therefore the instance does not notice about its changes.
Batch
T Rt.batch<T>(T Function() callback)
The batch
function allows you to combine multiple state changes to be grouped together, ensuring that any associated side effects are only triggered once, improving performance and reducing unnecessary re-renders. e.g.:
final stateA = UseState(0);
final stateB = UseState(0);
final computed = UseCompute(
() => stateA.value + stateB.value,
[stateA, stateB],
);
final batchReturned = Rt.batch(() {
stateA.value = 1;
stateB.value = 2;
print(computed.value); // 0 -> because the batch operation is not completed yet.
return stateA.value + stateB.value;
});
print(batchReturned); // 3
print(computed.value); // 3 -> because the batch operation is completed.
Batches can be nested and updates will be flushed when the outermost batch call completes. e.g.:
final stateA = UseState(0);
final stateB = UseState(0);
final computed = UseCompute(
() => stateA.value + stateB.value,
[stateA, stateB],
);
Rt.batch(() {
stateA.value = 1;
print(computed.value); // 0;
Rt.batch(() {
stateB.value = 2;
print(computed.value); // 0;
});
print(computed.value); // 0;
});
print(computed.value); // 3;
Untracked
T Rt.untracked<T>(T Function() callback)
The untracked
function helps you to execute the given callback
function without tracking any state changes. This means that any state changes that occur inside the callback
function will not trigger any side effects. e.g.:
final state = UseState(0);
final computed = UseCompute(() => state.value + 1, [state]);
Rt.untracked(() {
state.value = 2;
print(computed.value); // 1 -> because the state change is not tracked
});
print(computed.value); // 1 -> because the state change is not tracked
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 these generic arguments classes:
Args<A>
: represents one or more arguments ofA
type.Args1<A>
: represents a argument ofA
type.Args2<A, A2>
: represents two arguments ofA
,A2
type consecutively.Args3<A, A2, A3>
: represents three arguments ofA
,A2
,A3
type consecutively.ArgsX2<A>
: represents two arguments ofA
type.ArgsX3<A>
: represents three arguments ofA
type.
In each of the methods it provides these methods and properties:
arguments
: gets the list of arguments.toList<T>()
: gets the list of argumentsT
type.arg1
: gets the first argument.arg2
(Args2
,Args3
,ArgsX2
,ArgsX3
only): gets the second argument.arg3
(Args3
,ArgsX3
only): gets the third argument.
NOTE: If you need a generic argument class with more arguments, then create a new class following 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
ary
Function extention to convert anyFunction
with positional arguments toFunction
with 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 cache, 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 these properties:
computeValue
: represents a method that takes an argument of typeA
and returns a value ofT
type. This is the core function that will be memoized.interceptor
: receives aMemoInterceptor
that allows you to intercept the memoization function calls and modify the memoization process. Reactter providers some interceptors:MemoMultiInterceptor
: allows multiple memoization interceptors to be used together.MemoWrapperInterceptor
: a wrapper for a memoized function that allows you to define callbacks for initialization, successful completion, error handling, and finishing.MemoSafeAsyncInterceptor
: prevents saving in cache if theFuture
calculation function throws an error during execution.MemoTemporaryCacheInterceptor
: 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(int number) {
if (number == 0) return BigInt.one;
return BigInt.from(number) * factorialMemo(number - 1);
}
void main() {
// Returns the result of multiplication of 1 to 50.
final f50 = factorialMemo(50);
// Returns the result immediately from cache
// because it was resolved in the previous line.
final f10 = factorialMemo(10);
// Returns the result of the multiplication of 51 to 100
// and 50! which is obtained from the cache(as computed previously by f50).
final f100 = factorialMemo(100);
print(
'Results:\n'
'\t10!: $f10\n'
'\t50!: $f50\n'
'\t100!: $f100\n'
);
}
NOTE: The
computeValue
ofMemo
accepts one argument only. If you want to add more arguments, you can supply it using theRecord
(if your proyect support
) orgeneric arguments
(learn more here).
NOTE: Use
Memo.inline
in case there is a typing conflict, e.g. with theUseAsynState
andUseCompute
hooks which aFunction
type 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 (RtState
). However, it possesses distinct features that set them apart.
UseState
is a RtHook
, 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 RtProvider
or RtComponent
, and subsequently access it through BuildContext
. Conversely, with Signal
, which is inherently reactive, you can conveniently employ RtSignalWatcher
.
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
- Website Official
- Github
- Examples
- Examples in zapp
- Reactter documentation
- 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!