flutter_reactter 4.0.0-dev.4 flutter_reactter: ^4.0.0-dev.4 copied to clipboard
Reactter is a light, powerful and reactive state management.
A light, powerful and reactive state management.
Features #
- ⚡️ Build for speed.
- 📏 Reduce boilerplate code significantly.
- 📝 Improve code readability.
- 🪃 Unidirectional data flow.
- ⚛︎ Reactive state using Signal.
- ♻️ Reuse state creating custom hooks.
- 🪄 No configuration necessary.
- 🎮 Total control to re-render widget tree.
- 💙 Flutter or Dart only, you can use in any Dart project.
Let's see a small and simple example:
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
Reactter.on(count, Lifecycle.didUpdate, (_, __) => print('Count: $count'));
// Change the `value` in any time.
Future.doWhile(() async {
count.value++;
await Future.delayed(const Duration(seconds: 1));
return true;
});
// 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
- Usage
- Create a
ReactterContext
- Lifecycle of
ReactterContext
- Shortcuts to manage instances
- Shortcuts to manage events
- Using
UseContext
hook - Using
UseEvent
hook - Using
Signal
- Using
UseState
hook - Different between
UseState
andSignal
- Using
UseAsyncState
hook - Using
UseReducer
hook - Using
UseEffect
hook - Custom hook with
ReactterHook
- Create a
- Usage with
flutter_reactter
- Resources
- Documentation
- Examples
- Roadmap
- 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 project type you are making.
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';
Usage #
Create a ReactterContext
#
ReactterContext
is a abstract class that allows to manages ReactterHook
and provides life-cycle events.
In flutter, using ReactterProvider
, it's a way to share the state between widgets without having to explicitly pass a prop through every level of the tree.
You can use it's functionalities, creating a class that extends it:
class AppContext extends ReactterContext {}
RECOMMENDED: Name class with
Context
suffix, for easy locatily.
Lifecycle of ReactterContext
#
ReactterContext
has the following Lifecycle events:
-
Lifecycle.registered
: Event when the instance has registered byReactterInstanceManager
. -
Lifecycle.unregistered
: Event when the instance has unregistered byReactterInstanceManager
. -
Lifecycle.inicialized
: Event when the instance has inicialized byReactterInstanceManager
. -
Lifecycle.willMount
: Event when the instance will be mount in the widget tree (it use withflutter_reactter
only). -
Lifecycle.didMount
: Event when the instance did be mount in the widget tree (it use withflutter_reactter
only). -
Lifecycle.willUpdate
: Event when any instance's states will be update. Event param is aReactterState
. -
Lifecycle.didUpdate
: Event when any instance's states did be update. Event param is aReactterState
. -
Lifecycle.willUnmount
: Event when the instance will be unmount in the widget tree(it use withflutter_reactter
only). -
Lifecycle.destroyed
: Event when the instance did be destroyed byReactterInstanceManager
.
You can put it on listen, using Reactter.on
or UseEvent<T>().on
, for example:
Reactter.on(
ReactterInstance<AppContext>(),
Lifecycle.didUpdate,
(AppContext inst, ReactterState state) => print("Instance: $inst, state: $state"),
);
// or
UseEvent<AppContext>().on(
Lifecycle.didUpdate,
(AppContext inst, ReactterState state) => print("Instance: $inst, state: $state"),
);
Shortcuts to manage instances #
Reactter provides a some shortcuts to manage instances, these are:
-
Reactter.register
: Registers abuilder
function to allows to create the instance usingReactter.get
.Reactter.register(builder: () => AppContext()); Reactter.register(id: "uniqueId", builder: () => AppContext());
-
Reactter.unregister
: Removes thebuilder
function to avoid create the instance.Reactter.unregister<AppContext>(); Reactter.unregister<AppContext>("uniqueId");
-
Reactter.get
: Gets the previously instance created or creates a new instance from thebuild
registered usingreactter.register
.final appContext = Reactter.get<AppContext>(); final appContextWithId = Reactter.get<AppContext>(id: 'uniqueId');
-
Reactter.create
: Registers, creates and gets the instance directly.final appContext = Reactter.create(builder: () => AppContext()); final appContextWithId = Reactter.create(id: 'uniqueId', builder: () => AppContext());
-
Reactter.delete
: Deletes the instance but still keep thebuilder
function.Reactter.delete<AppContext>(); Reactter.delete<AppContext>('uniqueId');
NOTE: The registered instances have a global scope. This means that you can access them anywhere in the project just by using
Reactter.get
or throughUseContext
.
Shortcuts to manage events #
Reactter provides a some shortcuts to manage events, these are:
-
Reactter.on
: Puts on to listen event. When the event is emitted, thecallback
is called:void _onDidUpdate(inst, state) { print("Instance: $inst, state: $state"); } final appContext = Reactter.get<AppContext>(); Reactter.on(appContext, Lifecycle.didUpdate, _onDidUpdate); // or Reactter.on(ReactterInstance<AppContext>(), Lifecycle.didUpdate, _onDidUpdate);
-
Reactter.one
: Puts on to listen event only once. When the event is emitted, thecallback
is called and after removes event.void _onDestroyed(inst, _) { print("$inst was destroyed."); } Reactter.one(appContext, Lifecycle.destroyed, _onDestroyed); // or Reactter.one(ReactterInstance<AppContext>(), Lifecycle.destroyed, _onDestroyed);
-
Reactter.off
: Removes thecallback
of event.Reactter.off(appContext, Lifecycle.didUpdate, _onDidUpdate); // or Reactter.off(ReactterInstance<AppContext>(), Lifecycle.didUpdate, _onDidUpdate);
-
Reactter.emit
: Trigger event with or without theparam
given.Reactter.emit(appContext, CustomEnum.EventName, "test param"); // or Reactter.emit(ReactterInstance<AppContext>(), CustomEnum.EventName, "test param");
-
Reactter.emitAsync
: Trigger event with or without theparam
given as async way.await Reactter.emitAsync(appContext, CustomEnum.EventName, "test param"); // or await Reactter.emitAsync(ReactterInstance<AppContext>(), CustomEnum.EventName, "test param");
NOTE: The
ReactterInstance
helps to find the instance for event. This instance must have been created earlier in the Reactter context.RECOMMENDED: Use the instance directly on event methods for optimal performance.
Using UseContext
hook #
UseContext
is a ReactterHook
that allows to get ReactterContext
's instance when it's ready.
class AppContext extends ReactterContext {
late final otherContextHook = UseContext<OtherContext>(context: this);
// late final otherContextHookWithId = UseContext<OtherContext>(id: "uniqueId", context: this);
late otherContext = otherContext.instance;
AppContext() {
UseEffect(() {
otherContext = otherContextHook.instance;
}, [otherContextHook]);
}
}
NOTE: If you're not sure that you got the instance from the beginning, you need to use the
UseEffect
as shown in the example above.NOTE: The context that you need to get, must be created by
ReactterInstanceManager
.
Using UseEvent
hook #
UseEvent
is a ReactterHook
that manages events.
You can listen to event using on
method:
enum Events { SomeEvent };
void _onSomeEvent(inst, param) {
print("$inst's Events.SomeEvent emitted with param: $param.");
}
UseEvent<AppContext>().on(Events.SomeEvent, _onSomeEvent);
Use off
method to stop listening event:
UseEvent<AppContext>().off(Events.SomeEvent, _onSomeEvent);
If you want to listen event only once, use one
method:
UseEvent<AppContext>().one(Events.SomeEvent, _onSomeEvent);
And use emit
method to trigger event:
UseEvent<AppContext>().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 directly with
UseEvent.withInstance(<instance>)
for optimal performance.
Using Signal
#
Signal
is a class that store a value
of any type and notify the listeners when the value
is updated.
In flutter, using ReactterWatcher
, it's a way to keep the widgets automatically updates, accessing the value of signal reactively.
You can create a new Signal
, like so:
// usign `.signal` extension
final strSignal = "initial value".signal;
final intSignal = 0.signal;
final userSignal = User().signal;
// or usign the constructor class
final strSignal = Signal<String>("initial value");
final intSignal = Signal<int>(0);
final userSignal = Signal<User>(User());
Signal
has value
property that allows to read and write its state:
intSignal.value = 10;
print("Current state: ${intSignal.value}");
Or you can use it as a callable function:
intSignal(10);
print("Current state: ${intSignal()}");
Also, you can use toString
implict to access its state:
print("Current state: ${intSignal}");
NOTE:
Signal
notifies that its state has changed when the previous state is different from the current state. If its state is aObject
, not detect internal changes, only when states is setted anotherObject
.
If you want to notify changes after run a set of instructions, use update
method:
userSignal.update((user) {
user.firstname = "Leo";
user.lastname = "Leon";
});
If you want to force to notify changes, use refresh
method.
userSignal.refresh();
When value
is changed, the Signal
will emitted the following events:
Lifecycle.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecicle.didUpdate
is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
NOTE: When you do any arithmetic operation between two
Signal
s, its return aObj
, for example:1.signal + 2.signal
return3.obj
. AObj
is like aSignal
without reactive functionality, but you can convert it toSignal
using.toSignal
.
Using UseState
hook #
UseState
is a ReactterHook
that manages a state.
You can declarate it in the class, like so:
class AppContext extends ReactterContext {
final count = UseState(0);
}
Or outside the class, but you need to use the listenHooks
method, which is exposed by ReactterContext
:
final count = UseState(0);
class AppContext extends ReactterContext {
AppContext() {
listenHooks([count]);
}
}
NOTE: If you don't add context argument or use
listenHooks
, theReactterContext
won't be able to react to hook's changes.
UseState
has value
property that allows to read and write its state:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
AppContext() {
print("Prev state: ${count.value}");
count.value = 10;
print("Current state: ${count.value}");
}
}
NOTE:
UseState
notifies that its state has changed when the previous state is different from the current state. If its state is aObject
, not detect internal changes, only when states is setted anotherObject
.
If you want to notify changes after run a set of instructions, use update
method:
userState.update(() {
userState.value.firstname = "Leo";
userState.value.lastname = "Leon";
});
If you want to force to notify changes, use refresh
method.
userState.refresh();
When value
is changed, the UseState
will emitted the following events:
Lifecycle.willUpdate
event is triggered before thevalue
change orupdate
,refresh
methods have been invoked.Lifecicle.didUpdate
is triggered after thevalue
change orupdate
,refresh
methods have been invoked.
Different between UseState
and Signal
#
Both UseState
and Signal
represent a state(ReactterState
). But there are a few featues that are different between them.
UseState
is a ReactterHook
. This means that it doesn't work outside of ReactterContext
. Instead a Signal
can work both outside and inside a ReactterContext
. This is good for maintaining a global state or internal state if you use it into a ReactterContext
.
With UseState
is necessary use value
property every time for read or write its state. But with Signal
it is not necessary, improving code readability.
In Flutter, to use UseState
you need to provide its ReactterContext
to the Widget tree,with ReactterProvider
or ReactterComponent
and access it through of ContextBuilder
. With Signal
use ReactterWatcher
only, it's very simple.
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
into a ReactterContext
.
Using UseAsyncState
hook #
UseAsyncState
is a ReactterHook
with the same functionality as UseState
but provides a asyncValue
method, which will be obtained when resolve
method is executed.
class TranslateArgs {
final String to;
final String from;
final String text;
TranslateArgs({ this.to, this.from, this.text });
}
class AppContext extends ReactterContext {
late final translateState = UseAsyncStates<String, TranslateArgs>(
'Hello world',
translate
);
AppContext() {
_init();
}
Future<void> _init() async {
await translateState.resolve(
TranslateArgs({
to: 'ES',
from: 'EN',
text: translateState.value,
}),
);
print("'Hello world' translated to Spanish: '${translateState.value}'");
}
Future<String> translate([TranslateArgs args]) async {
return await api.translate(args);
}
}
NOTE: If you want to send argument to
asyncValue
method, need to define a type argument which be send throughresolve
method. Like the example shown above, the argument type send isTranslateArgs
class.
It also has when
method that returns a new value depending on it's state:
final valueComputed = asyncState.when<String>(
standby: (value) => "⚓️ Standby: $value",
loading: (value) => "⏳ Loading...",
done: (value) => "✅ Resolved: $value",
error: (error) => "❌ Error: $error",
);
Using UseReducer
hook #
UseReducer
is a ReactterHook
that manages state using reducer method. An alternative to UseState
.
RECOMMENDED:
UseReducer
is usually preferable toUseState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
UseReducer
accepts three arguments:
UseReducer(<reducer>, <initialState>, <context>);
- 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 with which you initialize the state. - The
context
represents any instance of theReactterContext
which is notified of any change in state.
UseReducer
exposes a dispatch
method that allows to invoke the reducer
method sending a ReactterAction
.
The current state can be access through value
property.
Here's the counter example:
class Store {
final int count;
Store({this.count = 0});
}
Store _reducer(Store state, ReactterAction<String, 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 AppContext extends ReactterContext {
late final state = UseReducer(_reducer, Store(count: 0), this);
AppContext() {
print("count: ${state.value.count}"); // count: 0;
state.dispatch(ReactterAction(type: 'INCREMENT', payload: 2));
print("count: ${state.value.count}"); // count: 2;
state.dispatch(ReactterAction(type: 'DECREMENT'));
print("count: ${state.value.count}"); // count: 1;
}
}
Also, you can create the actions as a callable class, extending from ReactterActionCallable
and use them like this:
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 AppContext extends ReactterContext {
late final state = UseReducer(_reducer , Store(count: 0), this);
AppContext() {
print("count: ${state.value.count}"); // count: 0;
state.dispatch(IncrementAction(quantity: 2));
print("count: ${state.value.count}"); // count: 2;
state.dispatch(DecrementAction());
print("count: ${state.value.count}"); // count: 1;
}
}
Using UseEffect
hook #
UseEffect
is a ReactterHook
that manages side-effect.
You can add it on constructor of class:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
AppContext() {
UseEffect((){
// Execute by count state changed or 'didMount' event
print("Count: ${count.value}");
Future.delayed(
const Duration(seconds: 1),
() => count.value += 1,
);
return () {
// Cleanup - Execute Before count state changed or 'willUnmount' event
print("Cleanup executed");
};
}, [count], this);
}
}
If you want to execute a UseEffect
immediately, use UseEffect.dispatchEffect
instead of the context
argument:
UseEffect(
() => print("Excute immediately or by hook's changes"),
[someState],
UseEffect.dispatchEffect
);
NOTE: If you don't add
context
argument toUseEffect
, thecallback
don't execute on lifecycledidMount
, and thecleanup
don't execute on lifecyclewillUnmount
(theses lifecycle events are used withflutter_reactter
only).
Custom hook with ReactterHook
#
ReactterHook
is a abstract class that allows to create a Custom Hook.
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 {
int _count = 0;
int get value => _count;
UseCount(int initial, [ReactterContext? context])
: _count = initial,
super(context);
void increment() => update(() => _count += 1);
void decrement() => update(() => _count -= 1);
}
RECOMMENDED: Name class with
Use
preffix, for easy locatily.NOTE:
ReactterHook
providesupdate
method which notify tocontext
that has changed.
and use it like that:
class AppContext extends ReactterContext {
late final count = UseCount(0, this);
AppContext() {
UseEffect(() {
Future.delayed(
const Duration(seconds: 1),
count.increment,
);
print("Count: ${count.value}");
}, [count], this);
}
}
Usage with flutter_reactter
#
Wrap with ReactterProvider
#
ReactterProvider
is a wrapper StatelessWidget
that provides a
ReactterContext
's instance to widget tree that can be access through the BuildContext
.
ReactterProvider(
() => AppContext(),
builder: (context, child) {
final appContext = context.watch<AppContext>();
return Text("count: ${appContext.count.value}");
},
)
If you want to create a different ReactterContext
's instance, use id
parameter.
ReactterProvider(
() => AppContext(),
id: "uniqueId",
builder: (context, child) {
final appContext = context.watchId<AppContext>("uniqueId");
return Text("count: ${appContext.count.value}");
},
)
IMPORTANT: Dont's use
ReactterContext
with constructor parameters to prevent conflicts. Instead useonInit
method to access its instance and put the data you need.NOTE:
ReactteProvider
is a "scoped". So it contains aReactterScope
which thebuilder
callback will be rebuild, when theReactterContext
changes. For this to happen, theReactterContext
should put it on listens forBuildContext
'swatch
ers.
Access to ReactterContext
#
Reactter provides additional methods to BuildContext
to access your ReactterContext
. These are following:
context.watch
: Gets theReactterContext
's instance from the closest ancestor ofReactterProvider
and watch allReactterHook
orReactterHook
defined in first paramater.
final watchContext = context.watch<WatchContext>();
final watchHooksContext = context.watch<WatchHooksContext>(
(ctx) => [ctx.stateA, ctx.stateB],
);
context.watchId
: Gets theReactterContext
's instance withid
from the closest ancestor ofReactterProvider
and watch allReactterHook
orReactterHook
defined in second paramater.
final watchIdContext = context.watchId<WatchIdContext>('id');
final watchHooksIdContext = context.watchId<WatchHooksIdContext>(
'id',
(ctx) => [ctx.stateA, ctx.stateB],
);
context.use
: Gets theReactterContext
's instance with/withoutid
from the closest ancestor ofReactterProvider
.
final readContext = context.use<ReadContext>();
final readIdContext = context.use<ReadIdContext>('id');
NOTE: These methods mentioned above uses
ReactterProvider.contextOf
NOTE:
context.watch
andcontext.watchId
watch all or some of the specifiedReactterHook
dependencies and when it will change, re-render widgets in the scope ofReactterProviders
,ReactterBuilder
or any Widget that exposes theContextBuilder
likeBuild
,StatelessWidget
,StatefulWidget
.
React to Signal
s with ReactterWatcher
#
ReactterWatcher
is a Statefulwidget
that listens for Signal
s and re-build when any Signal
is changed.
final count = 0.signal;
final flag = false.signal;
void increase() => count.value += 1;
void toggle() => flag(!flag.value);
class Example extends StatelessWidget {
...
Widget build(context) {
return ReactterWatcher(
child: Row(
children: const [
ElevatedButton(
onPressed: increase,
child: Text("Increase"),
),
ElevatedButton(
onPressed: toggle,
child: Text("Toogle"),
),
],
),
builder: (context, child) {
// This rebuild the widget tree when `count` or `flag` are updated.
return Column(
children: [
Text("Count: $count"),
Text("Flag is: $flag"),
// This takes the widget from the `child` property in each rebuild.
child,
],
);
},
);
}
}
Control re-render with ReactterBuilder
#
ReactterBuilder
is a StatelessWidget
that to to get the ReactterContext
's instance from the closest ancestor of ReactterProvider
and exposes it through the first parameter of builder
callback.
ReactterBuilder<AppContext>(
listenAll: true,
builder: (appContext, context, child) {
return Text("Count: ${appContext.count.value}");
},
)
NOTE:
ReactterBuilder
is read-only by default(listenAll: false
), this means it only renders once. Instead uselistenAll
astrue
or uselistenStates
with theReactterHook
s specific and then thebuilder
callback will be rebuild with everyReactterContext
'sReactterHook
changes.NOTE:
ReactterBuilder
is a "scoped". So, thebuilder
callback will be rebuild, when theReactterContext
changes or anyReactterHook
specified. For this to happen, theReactterContext
should put it on listens forBuildContext
'swatch
ers.
Multiple ReactterProvider
with ReactterProviders
#
ReactterProviders
is a wrapper StatelessWidget
that allows to use multiple ReactterProvider
as nested way.
ReactterProviders(
[
ReactterProvider(() => AppContext()),
ReactterProvider(
() => ConfigContext(),
id: 'App',
onInit: (appConfigContext) {
appConfigContext.config.value = 'new state';
},
),
ReactterProvider(
() => ConfigContext(),
id: 'User'
),
],
builder: (context, child) {
final appContext = context.watch<AppContext>();
final appConfigContext = context.watchId<ConfigContext>('App');
final userConfigContext = context.watchId<ConfigContext>('User');
...
},
)
Create a ReactterComponent
#
ReactterComponent
is a abstract StatelessWidget
class that provides the functionality of ReactterProvider
with a ReactterContext
and exposes it through render
method.
class CounterComponent extends ReactterComponent<AppContext> {
const CounterComponent({Key? key}) : super(key: key);
@override
get builder => () => AppContext();
@override
get id => 'uniqueId';
@override
listenStates(appContext) => [appContext.stateA];
@override
Widget render(appContext, context) {
return Text("StateA: ${appContext.stateA.value}");
}
}
Resources #
Roadmap #
We want to keeping adding features for Reactter
, those are some we have in mind order by priority:
- Widget to control re-render using only hooks
- Async context.
- Do benchmarks and improve performance.
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!