flutter_reactter 3.0.0-dev.4
flutter_reactter: ^3.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.
- ♻️ Reuse state using custom hooks.
- 🪄 No configuration necessary.
- 🎮 Total control to re-render widget tree.
- 💙 Flutter or Dart only, you can use in any Dart project.
Contents #
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.
You can refer to the following table to help you decide which package to use:
| Project type | Packages |
|---|---|
| Dart only | |
| Flutter |
Once you know what package you want to install, proceed to add the package on your project:
-
With command:
dart pub add reactterFor flutter:
flutter pub add flutter_reactter -
Or add a line like this into your
pubspec.yamlfile:dependencies: reactter: #add version hereand then run
dart pub get.For flutter:
dependencies: flutter_reactter: #add version hereand then run
flutter pub get.
Now in your Dart code, you can use:
import 'package:reactter/reactter.dart';
for flutter:
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.
You can use it's functionalities, creating a class that extends it:
class AppContext extends ReactterContext {}
RECOMMENDED: Name class with
Contextsuffix, 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_reactteronly). -
Lifecycle.didMount: Event when the instance did be mount in the widget tree (it use withflutter_reactteronly). -
Lifecycle.willUpdate: Event when any instance's hooks will be update. Event param is aReactterHook. -
Lifecycle.didUpdate: Event when any instance's hooks did be update. Event param is aReactterHook. -
Lifecycle.willUnmount: Event when the instance will be unmount in the widget tree(it use withflutter_reactteronly). -
Lifecycle.destroyed: Event when the instance did be destroyed byReactterInstanceManager.
You can put it on listen, using UseEvent, for example:
UseEvent<AppContext>().on<ReactterHook>(
Lifecycle.didUpdate,
(inst, hook) => print("Instance: $inst, hook: $hook),
);
Manage instance with ReactterInstanceManage #
ReactterInstanceManager is a extension of Reactter that exposes some methods to helps to manages instance. These are some methods:
Reactter.register: Registers a builder function to allows to create the instance using Reactter.get.
Reactter.register(builder: () => AppContext());
Reactter.register(id: "uniqueId", builder: () => AppContext());
Reactter.unregister: Removes the builder function to avoid create the instance.
Reactter.unregister<AppContext>();
Reactter.unregister<AppContext>("uniqueId");
Reactter.get: Gets the previously instance created or create a new instance from the build registered using reactter.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(build: () => AppContext());
final appContextWithId = Reactter.create(id: 'uniqueId', build: () => AppContext());
Reactter.delete: Deletes the instance but still keep the build function.
Reactter.delete<AppContext>();
Reactter.delete<AppContext>(id: 'uniqueId');
Using UseContext hook #
UseContext is a ReactterHook that allows to get ReactterContext's instance when ready.
class AppContext extends ReactterContext {
late final otherContextHook = UseContext<OtherContext>(context: this);
// 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
UseEffectas shown in the example above.NOTE: The context that you need to get, must be created by
ReactterInstanceManager.
Using UseEvent hook #
UseEvent is a hook 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
offor usingdisposeto remove all instance's events. Failure to do so could increase memory usage or have unexpected behaviors such as events in permanent listening.RECOMMENDED: It you have the instances, use directly with
UseEvent.withInstance(Instance).
Using UseState hook #
UseState is a ReactterHook that manages a state.
You can add it on any part of class, with context argument(this) to put this hook on listen:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
}
or add it on listenHooks method which ReactterContext exposes it:
class AppContext extends ReactterContext {
final count = UseState(0);
AppContext() {
listenHooks([count]);
}
}
NOTE: If you don't add context argument or use
listenHook, theReactterContextwon't be able to react to hook's changes.
UseState exposes value property that allows to read and writter 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:
UseStatenotifies 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 anotherObject.NOTE: If you want to force notify, execute
updatemethod whichUseStateexposes it.
Using UseAsyncState hook #
UseAsyncState is a ReactterHook with the same functionality as UseState but provides a asyncValue which it will be obtain when resolve method is executed.
class AppContext extends ReactterContext {
late final asyncState = UseAsyncState<String?, Arguments>(null, _resolveState, this);
AppContext() {
_init();
}
Future<void> _init() async {
await asyncState.resolve(
Arguments(prop: true, prop2: "test"),
);
print("State resolved with: ${state.value}");
}
Future<String> _resolveState([Arguments args]) async {
return await api.getState(args.prop, args.prop2);
}
}
NOTE: If you want send argument to
asyncValuemethod, need to defined a type arg which its send fromresolvemethod. Like example shown above, which type argument send isArgumentsclass.
It also has when method that returns a new value depending of it's state:
final valueComputed = asyncState.when<String>(
standby: (value) => "⚓️ Standby: $value",
loading: (value) => "⏳ Loading...",
done: (value) => "✅ Resolved: $value",
error: (error) => "❌ Error: $error",
);
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"),
[someHook],
UseEffect.dispatchEffect
);
NOTE: If you don't add
contextargument toUseEffect, thecallbackdon't execute on lifecycledidMount, and thecleanupdon't execute on lifecyclewillUnmount(theses lifecycle events are used withflutter_reactteronly).
Create a ReactterHook #
ReactterHook is a abstract class that allows to create a custom hook.
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
Usepreffix, for easy locatily.NOTE:
ReactterHookprovidesupdatemethod which notify tocontextthat has changed.
and use it like that:
class AppContext extends ReactterContext {
late final count = UseCount(0, this);
AppContext() {
UseEffect(() {
Future.delayed(
const Duration(secounds: 1),
count.increment,
);
print("Count: ${count.value}");
}, [count], this);
}
}
Global state #
The reactter's hooks can be defined as static to access its as global way:
class Global {
static final flag = UseState(false);
static final count = UseCount(0);
// Create a class factory to run it as singleton way.
// This way, the initial logic can be executed.
static final Global _inst = Global._init();
factory Global() => _inst;
Global._init() {
UseEffect(
() async {
await Future.delayed(const Duration(seconds:s 1));
doCount();
},
[count],
UseEffect.dispatchEffect,
);
}
static void doCount() {
if (count.value <= 0) {
flag.value = true;
}
if (count.value >= 10) {
flag.value = false;
}
flag.value ? count.increment() : count.decrement();
}
}
// It's need to create the instance it to be able
// to execute Global._init(This executes only once).
final global = Global();
This is a example that how you could use it:
class AppContext extends ReactterContext {
late final isOdd = UseState(false, this);
AppContext() {
UseEffect((){
isOdd.value = Global.count.value % 2 != 0;
}, [Global.count], this);
}
}
NOTE: If you want to execute some logic when initialize the global class you need to use the class factory and then instance it to run as singleton way.
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: Don's use
ReactterContextwith constructor parameters to prevent conflicts. Instead useonInitmethod to access its instance and put the data you need.NOTE:
ReactteProvideris a "scoped". So it contains aReactterScopewitch thebuildercallback will be rebuild, when theReactterContextchanges. For this to happen, theReactterContextshould put it on listens forBuildContext'swatchers.
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 ofReactterProviderand watch allReactterHookorReactterHookdefined in first paramater.
final watchContext = context.watch<WatchContext>();
final watchHooksContext = context.watch<WatchHooksContext>(
(ctx) => [ctx.stateA, ctx.stateB],
);
context.watchId: Gets theReactterContext's instance withidfrom the closest ancestor ofReactterProviderand watch allReactterHookorReactterHookdefined in second paramater.
final watchIdContext = context.watchId<WatchIdContext>('id');
final watchHooksIdContext = context.watchId<WatchHooksIdContext>(
'id',
(ctx) => [ctx.stateA, ctx.stateB],
);
context.read: Gets theReactterContext's instance from the closest ancestor ofReactterProvider.
final readContext = context.read<ReadContext>();
context.readId<T>: Gets theReactterContext's instance withidfrom the closest ancestor ofReactterProvider
final readIdContext = context.readId<ReadIdContext>('id');
NOTE: These methods mentioned above uses
ReactterProvider.contextOfNOTE:
context.watchandcontext.watchIdwatch all or some of the specifiedReactterHookdependencies and when it will change, re-render widgets in the scope ofReactterProviders,ReactterBuilderorReactterScope.
Control re-render with ReactterScope #
ReactterScope is a wrapeer StatelessWidget that helps to control re-rendered of widget tree.
ReactterScope<AppContext>(
builder: (context, child) {
final appContext = context.watch<AppContext>();
return Text("Count: ${appContext.count.value}");
},
)
NOTE: The
buildercallback will be rebuild, when theReactterContextchanges. For this to happen, theReactterContextshould put it on listens forBuildContext'swatchers.
Control re-render with ReactterBuilder #
ReactterBuilder is a wrapper StatelessWidget that helps to get the ReactterContext's instance from the closest ancestor of ReactterProvider and exposes it through the first parameter of builder callback.
ReactterBuilder<AppContext>(
listenAllHooks: true,
builder: (appContext, context, child) {
return Text("Count: ${appContext.count.value}");
},
)
NOTE:
ReactterBuilderis read-only by default(listenAllHooks: false), this means it only renders once. Instead uselistenAllHooksastrueor uselistenHookswith theReactterHooks specific and then thebuildercallback will be rebuild with everyReactterContext'sReactterHookchanges.NOTE:
ReactterBuilderis a "scoped". So it contains aReactterScopewitch thebuildercallback will be rebuild, when theReactterContextchanges. For this to happen, theReactterContextshould put it on listens forBuildContext'swatchers.
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
listenHooks(appContext) => [appContext.stateA];
@override
Widget render(appContext, context) {
return Text("StateA: ${appContext.stateA.value}");
}
}
Resources #
Roadmap #
We want keeping adding features for Reactter, those are some we have in mind order by priority:
- Async context.
- Structure proposal for large projects.
- Do benchmarks.
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.
- Write articles or make videos teaching how to use Reactter.
Any idea is welcome!
Authors #
- Leo Castellanos - leoocast.dev@gmail.com
- Carlos León - carleon.dev@gmail.com