reactter 6.0.0-dev.3 reactter: ^6.0.0-dev.3 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 #
- ⚡️ Build for speed.
- ⚖️ Super lightweight(🥇 See benchmarks).
- 📏 Reduce boilerplate code significantly(🥇 See benchmarks).
- 📝 Improve code readability.
- 💧 Flexible and adaptable to any architecture.
- ☢️ Reactive state using Signal.
- ♻️ Reuse state creating custom hooks.
- 🎮 Total control to re-render widget tree.
- 🧪 Fully testable, 100% code coverage.
- 🪄 No configuration and no code generation necessary.
- 💙 Dart or Flutter, supports the latest version of Dart.
Let's see a small and simple example:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_reactter/flutter_reactter.dart';
// 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.
Timer.periodic(Duration(seconds: 1), (_) => count.value++);
// And you can use in flutter like this:
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
- Shortcuts to manage instances
- UseContext
- ReactterProvider (
flutter_reactter
) - ReactterProviders (
flutter_reactter
) - ReactterComponent (
flutter_reactter
)
- LifeCycle and event management
- Shortcuts to manage events
- UseEvent
- UseEffect
- ReactterConsumer (
flutter_reactter
) - ReactterWatcher (
flutter_reactter
) - BuildContext extension (
flutter_reactter
)
- Custom hooks
- Generic arguments
- 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:
State management #
In Reactter, state is understood as any object that extends ReactterState
, which gives it features such as being able to store one or more values and to notify of its changes.
Reactter offers the following several state managers:
NOTE: The hooks (also known as
ReactterHook
) 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 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:
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:1.signal + 2.signal
returns3.obj
. AnObj
is like aSignal
without reactive functionality, but you can convert it toSignal
using.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
accepts an argument:
UseState<T>(T initialValue)
It can be declared inside a class, like this:
class CounterController {
final count = UseState(0);
}
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 (ReactterHook
) with the same feature as UseState
but its value will be lazily resolved by a function(asyncFunction
).
UseAsyncState
accepts two arguments:
UseAsyncState<T>(
T initialValue,
Future<T> Function() asyncFunction,
);
Use UseAsyncState.withArg
to pass arguments to the asyncFunction
UseAsyncState.withArg<T, A extends Arg?>(
T initialValue,
Future<T> Function(A) asyncFunction,
)
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}'");
});
}
}
// TODO: to reference #generic-arguments
and how is it used in the example
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(ReactterHook
) 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
accepts two arguments:
UseReducer<T>(
T Function(T state, ReactterAction<dynamic> action) reducer,
T initialState,
);
- The
reducer
method contains your custom state logic that calculates the new state using current state, and actions. - The
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 Store {
final int count;
Store({this.count = 0});
}
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) =>
action is ReactterActionCallable ? action(state) : UnimplementedError();
class CounterController {
final useCounter = UseReducer(reducer , Store(count: 0));
CounterController() {
print("count: ${useCounter.value.count}"); // count: 0;
useCounter.dispatch(IncrementAction(quantity: 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(ReactterHook
) that keeps listening for state dependencies
changes, to return a computed value(T
) from a defined method(computeValue
).
UseCompute
accepts two arguments:
UseCompute<T>(
T Function() computeValue,
List<ReactterState> dependencies,
)
An example:
class AppController {
final stateA = UseState(1);
final stateB = UseState(7);
late final computeState = UseCompute(
() => (stateA + stateB).clamp(10, 15),
[stateA, stateB],
);
AppController() {
print(computeState.value); // 10;
// will print: 11, 15, 11
Reactter.on(
computeState,
Lifecycle.didUpdate,
(_, __) => print(computeState.value),
);
stateA.value += 1; // computeState doesn't change, its value is 10
stateB.value += 2; // computeState changes, its value is 11
stateA.value += 4; // computeState changes, its value is 15
stateB.value += 8; // computeState doesn't change, its value is 15
stateA.value -= 8; // computeState doesn't change, its value is 15
stateB.value -= 4; // computeState changes, its value is 11
}
}
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 theUseCompute
method.
// TODO: to talk about the performance using Reactter.memo
ReactterMemo #
ReactterMemo
is a class callable that lets you cache the result of a calculation.
ReactterMemo
accept an argument:
ReactterMemo<T, A extends Arg?>(
T Function(A) calculateValue,
)
When ReactterMemo
is called, it takes the arguments(A
) and check if value(T
) was previously resolved by calculateValue
using the same arguments(A
), then returns value(T
) from the cache, otherwise it calls calculateValue
and stores the returned value(T
) in the cache.
This is an factorial example:
late final factorial = ReactterMemo(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) * factorial(Arg(numero - 1));
}
void main() {
// Returns the result of multiplication of 1 to 50.
final f50 = factorial(const Arg(50));
// Returns the result immediately from cache
// because it was resolved in the previous line.
final f10 = factorial(const Arg(10));
// Returns the result of the multiplication of 51 to 100
// and 50! which is obtained from the cache.
final f100 = factorial(const Arg(100));
print(
'Results:\n'
'\t10!: $f10\n'
'\t50!: $f50\n'
'\t100!: $f100\n'
);
}
// TODO: to add Reactter.memo
shortcut and what are the differences
// TODO: to reference #generic-arguments
// TODO: to talk about the performance
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 offers the following several instance managers:
Shortcuts to manage instances #
Reactter offers several convenient shortcuts for managing instances:
-
Reactter.register
: This method registers abuilder
function, enabling the creation of an instance usingReactter.get
.Reactter.register(builder: () => AppController()); Reactter.register(id: "uniqueId", builder: () => AppController());
-
Reactter.unregister
: This method removes thebuilder
function, preventing the creation of the instance.Reactter.unregister<AppController>(); Reactter.unregister<AppController>("uniqueId");
-
Reactter.get
: This method retrieves a previously created instance or creates a new instance from thebuild
registered withreactter.register
.final appController = Reactter.get<AppController>(); final appControllerWithId = Reactter.get<AppController>('uniqueId');
-
Reactter.create
: This method registers, creates and retrieves the instance directly.final appController = Reactter.create(builder: () => AppController()); final appControllerWithId = Reactter.create(id: 'uniqueId', builder: () => AppController());
-
Reactter.delete
: This method deletes the instance but keeps thebuilder
function.Reactter.delete<AppController>(); Reactter.delete<AppController>('uniqueId');
NOTE: The scope of the registered instances is global. This indicates that using
Reactter.get
orUseContext
will allow you to access them from anywhere in the project.
UseContext #
UseContext
is a hook(ReactterHook
) that allows to get the T
instance with/without id
from the dependency store when it's ready.
class AppController {
final useAuthController = UseContext<AuthController>();
// final useOtherControllerWithId = UseContext<OtherController>("UniqueId");
AuthController? authController = useAuthController.instance;
AppController() {
UseEffect(() {
authController = useAuthController.instance;
}, [useAuthController]);
}
}
Use instance
getter to get the T
instance.
Use UseEffect
hook as shown in the example above, to wait for the instance
to be created.
NOTE: The instance that you need to get, must be created by
Dependency injection
before.
ReactterProvider #
ReactterProvider
is a Widget (exclusive of flutter_reactter
) that hydrates from an T
instance to the Widget tree. The T
instance can be accessed through methods BuildContext extension:
ReactterProvider<CounterController>(
() => CounterController(),
builder: (counterController, context, child) {
context.watch<CounterController>();
// `context.watch` listens any CounterController changes for rebuild
return Text("count: ${counterController.count.value}");
},
)
Uses id
property to identify the T
instance.
Use the child
property to pass a Widgetthat will be built only once.
It will be sent through the builder
method, so you can incorporate it into your build.
RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
NOTE:
ReactteProvider
is "scoped". So, thebuilder
method will be rebuild when the instance or anyReactterState
specified using 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:
ReactteProviders
is "scoped". So, thebuilder
method will be rebuild, when the instance or anyReactterState
specified using the watch methods of BuildContext extension changes.
ReactterComponent #
ReactterComponent
is a Widget (exclusive of flutter_reactter
) that provides ReactterProvider
features, whose T
instance defined 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 creating method.
NOTE: If you don't use
builder
getter, the instanceT
is not created. Instead, an attempt will be made to locate it within the closest ancestor where it was initially created. RECOMMENDED: Don't use Object with constructor parameters to prevent conflicts.
Use id
getter to identify the T
instance:
Use listenStates
getter to define the states and with its changes rebuild the Widget tree defined in the render
method.
Use listenAll
getter as true
to listen to all the T
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 are as follows:
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:
- Shortcuts to manage events
- UseEvent
- UseEffect
- ReactterConsumer
- ReactterWatcher
- BuildContext extension
Shortcuts to manage events #
Reactter offers several convenient shortcuts for managing events:
-
Reactter.on(<Object inst>, <enum event>, <Fuction callback>)
: Turns on the listen event. When the event(enum
) of instance(Object
) is emitted, thecallback
is called:void onDidUpdate(inst, state) => print("Instance: $inst, state: $state"); final appController = Reactter.get<AppController>(); Reactter.on(appController, Lifecycle.didUpdate, onDidUpdate); // or Reactter.on(ReactterInstance<AppController>(), Lifecycle.didUpdate, onDidUpdate);
-
Reactter.one(<Object inst>, <enum event>, <Fuction callback>)
: Turns on the listen event for only once. After the event(enum
) of instance(Object
) is emitted, thecallback
is called and ended.void onDidUpdate(inst, state) => print("Instance: $inst, state: $state"); final appController = Reactter.get<AppController>(); Reactter.on(appController, Lifecycle.didUpdate, onDidUpdate); // or Reactter.on(ReactterInstance<AppController>(), Lifecycle.didUpdate, onDidUpdate);
-
Reactter.off(<Object inst>, <enum event>, <Fuction callback>)
: Removes thecallback
from event(enum
) of instance(Object
).Reactter.off(appController, Lifecycle.didUpdate, onDidUpdate); // or Reactter.off(ReactterInstance<AppController>(), Lifecycle.didUpdate, onDidUpdate);
-
Reactter.emit(<Object inst>, <enum event>, <dynamic param>)
: Triggers an event(enum
) of instance(Object
) with or without theparam
given.Reactter.emit(appController, CustomEnum.EventName, "test param"); // or Reactter.emit(ReactterInstance<AppController>(), CustomEnum.EventName, "test param");
-
Reactter.emitAsync(<Object inst>, <enum event>, <dynamic param>)
: Triggers an event(enum
) of instance(Object
) with or without theparam
given as async way.await Reactter.emitAsync(appController, CustomEnum.EventName, "test param"); // or await Reactter.emitAsync(ReactterInstance<AppController>(), CustomEnum.EventName, "test param");
NOTE: The
ReactterInstance
helps to find the instance for event.RECOMMENDED: Use the instance directly on event methods for optimal performance.
UseEvent #
UseEvent
is a hook (ReactterHook
) that allows to manager events.
Use the on
method to listen for instance's event:
enum Events { SomeEvent };
void onSomeEvent(inst, param) {
print("$inst's Events.SomeEvent emitted with param: $param.");
}
UseEvent<AppController>().on(Events.SomeEvent, onSomeEvent);
Use the off
method to stop listening to instance's event:
UseEvent<AppController>().off(Events.SomeEvent, onSomeEvent);
Use the one
method to listen for instance's event once:
UseEvent<AppController>().one(Events.SomeEvent, onSomeEvent);
Use the emit
method to trigger an instance event:
UseEvent<AppController>().emit(Events.SomeEvent, 'Parameter');
IMPORTANT: Don't forget to remove event using
off
or usingdispose
to remove all instance's events. Failure to do so could increase memory usage or have unexpected behaviors, such as events in permanent listening.RECOMMENDED: If you have the instance, use it directly with
UseEvent.withInstance(<instance>)
for optimal performance.
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
instance
argument toUseEffect
, thecallback
won't execute on lifecycledidMount
, and thecleanup
won't execute on lifecyclewillUnmount
(thesesLifeCycle
events are used withflutter_reactter
only).
ReactterConsumer #
ReactterConsumer
is a Widget (exclusive of flutter_reactter
) that allows to access the T
instance from ReactterProvider
's nearest ancestor and can listen all or specified states to rebuild the Widget when theses changes occur:
class SomeWidget extends StatelessWidget {
...
Widget build(context) {
return ReactterConsumer<AppController>(
builder: (appController, context, child) {
// This is built once only.
return Text("My instance: $appContoller");
}
);
}
}
Use listenAll
property as true
to listen all the T
instance changes to rebuild the Widget tree defined in builder
method:
class SomeWidget extends StatelessWidget {
...
Widget build(context) {
return ReactterConsumer<AppController>(
listenAll: true,
builder: (appController, context, child) {
// This is built every change that occurs.
return Text("My instance: $appContoller - ${DateTime.now()}");
}
);
}
}
Use listenStates
property to define the states and with its changes rebuild the Widget tree defined in builder
method:
class SomeWidget extends StatelessWidget {
...
Widget build(context) {
return ReactterConsumer<AppController>(
listenStates: (inst) => [inst.stateA, inst.stateB],
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}"),
],
);
}
);
}
}
NOTE: You can use
List<ReactterState>.when
extension for more specific conditional state when you want the widget tree to be re-rendered. For example:
class SomeWidget 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 built according to the above conditions.
return Column(
children: [
Text("My instance: $appContoller"),
Text("StateA: ${appContoller.stateA.value}"),
Text("StateB: ${appContoller.stateB.value}"),
],
);
}
);
}
}
Uses id
property to identify the T
instance.
Use child
property to pass a Widget which to be built once only.
It will be sent through the builder
method, so you can incorporate it into your build.
ReactterWatcher #
ReactterWatcher
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:
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"),
// Takes the Widget from the `child` property in each rebuild.
child,
],
);
},
);
}
}
BuildContext extension #
Reactter provides additional methods through BuildContext
to access to instance. These are following:
context.watch
: Gets theT
instance fromReactterProvider
's nearest ancestor and listens any instance changes orReactterState
changes 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>.when
extension 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 theT
instance byid
fromReactterProvider
's nearest ancestor and listens instance changes orReactterState
changes declared in second paramater.
// listens any `ResourceController` by `id` changes
final resourceController = context.watchId<ResourceController>('UniqueId');
// listens the states changes declared in second paramater.
final resourceController = context.watchId<ResourceController>(
'UniqueId',
(inst) => [inst.stateA, inst.stateB],
);
context.use
: Gets theT
instance with/withoutid
fromReactterProvider
's nearest ancestor.
final appController = context.use<AppController>();
final resourceController = context.use<ResourceController>('UniqueId');
NOTE: These methods mentioned above uses
ReactterProvider.contextOf
NOTE:
context.watch
andcontext.watchId
watch all or some of the specifiedReactterState
dependencies, when any it will changes, re-built the Widgets tree in the scope ofReactterProvider
,ReactterComponent
or any Widget that exposes theBuildContext
likeBuild
,StatelessWidget
,StatefulWidget
.NOTE: A
ReactterState
can be aSignal
orReactterHook
(likeUseState
,UseAsynState
,UseReducer
or 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:
ReactterHook
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 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}",
);
}
}
Generic arguments #
// TODO: to complete documentation
Difference between Signal and UseState #
Both UseState
and Signal
represent a state (ReactterState
). But there are a few features that are different between them.
UseState
is a ReactterHook
, therefore unlike a Signal
, it can be extended and given new capabilities.
Use of the value
attribute is required each time UseState
reads or writes its state. However, Signal
eliminates the need for it, making the code easier to understand.
In Flutter, when you want to use UseState
, you must expose the containing parent class to the widget tree through a ReactterProvider
or ReactterComponent
and access it using BuildContext
. Instead, with Signal
which is reactive, you simply use ReactterWatcher
.
But it is not all advantages for Signal
, although it is good for global states and for improving code readability, it is prone to antipatterns and makes debugging difficult (This will be improved in the following versions).
The decision between which one to use is yours. You can use one or both without them getting in the way. And you can even replace a UseState
with a Signal
at any time.
Resources #
- Documentation
- Examples
- Examples in zapp
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 idea is welcome!