reactter 2.2.0-dev.1
reactter: ^2.2.0-dev.1 copied to clipboard
Reactter is a light, powerful and reactive state management.
A light, powerful and reactive state management.
By using Reactter you get:
- Reduce significantly boilerplate code.
- Improve code readability.
- Unidirectional data flow.
- Control re-render widget tree.
- Reuse state using custom hooks.
Contents #
Quickstart #
In your flutter project add the dependency:
Run this command with Flutter:
flutter pub add reactter
or add a line like this to your package's pubspec.yaml:
dependencies:
reactter: ^2.1.0
Now in your Dart code, you can use:
import 'package:reactter/reactter.dart';
Usage #
Create a ReactterContext #
ReactterContext is a abstract class with functionality to manages hooks (like UseState, UseEffect) and lifecycle 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.
Using UseState hook #
UseState is a hook that allow to manage a state.
INFO: The different with other management state is that not use
Stream. We know thatStreamconsumes a lot of memory and we had decided to use the simple publish-subscribe pattern.
You can add it on any part of class, with context(this) argument(RECOMMENDED):
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 add
UseStatewithcontextargument, not need to add it onlistenHooks, but is required declarate it aslate.
UseState exposes value property that helps 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}")
}
}
A UseState notifies that its state has changed when the previous state is different from the current state.
NOTE: If its state is a
Object, not detect internal changes, only when states is anotherObject.NOTE: If you want to force notify, execute
updatemethod whichUseStateexposes it.
Using UseEffect hook #
UseEffect is a hook that allow to manage side-effect.
You can add it on constructor of class:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
late final isOdd = UseState(false, this);
AppContext() {
UseEffect((){
isOdd.value = count.value % 2 != 0;
}, [count], this);
}
}
NOTE: If you don't add
contextargument toUseEffect, thecallbackdon't execute on lifecyclewillMount, and thecleanupdon't execute on lifecyclewillUnmount.NOTE: If you want to execute a
UseEffectimmediately, useUseEffect.dispatchEffectinstead of thecontextargument.
Wrap with ReactterProvider and UseContext #
ReactterProvider is a wrapper widget of a InheritedWidget witch helps exposes the ReactterContext that are defined using UseContext on contexts parameter.
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ReactterProvider(
contexts: [
UseContext(
() => AppContext(),
),
UseContext(
() => ConfigContext(),
id: 'App',
onInit: (appConfigContext) {
appConfigContext.config.value = 'new state';
},
),
UseContext(
() => ConfigContext(),
id: 'User'
),
],
builder: (context, _) {
final appContext = context.of<AppContext>();
final appConfigContext = context.ofId<ConfigContext>('App');
final userConfigContext = context.ofId<ConfigContext>('User');
return [...]
},
);
}
}
For more information about context.of[...] go to here.
RECOMMENDED: Don't use
ReactterContextclass with parameters to prevent conflicts. Instead of it, useonInitmethod whichUseContextexposes for access its instance and put the data you need.NOTE: You can use
idparameter ofUseContextfor create a different instance of sameReactterContextclass.
Control re-render with ReactterBuilder #
ReactterBuilder create a scope where isolates the widget tree which will be re-rendering when all or some of the specified ReactterHook dependencies on listenHooks has changed.
ReactterProvider(
contexts: [
UseContext(() => AppContext()),
],
builder: (context, child) {
// This builder is render only one time.
// But if you use context.of<T> or context.ofId<T> here,
// it forces re-render this builder together ReactterBuilder's builder.
final appContext = context.ofStatic<AppContext>();
return Column(
children: [
Text("stateA: ${appContext.stateA.value}"),
ReactterBuilder<AppContext>(
listenHooks: (appContext) => [appContext.stateB],
builder: (appContext, context, child){
// This builder is re-render when only stateB changes
return Text("stateB: ${appContext.stateB.value}");
},
),
ReactterBuilder<AppContext>(
listenHooks: (appContext) => [appContext.stateC],
builder: (appContext, context, child){
// This builder is re-render when only stateC changes
return Text("stateC: ${appContext.stateC.value}");
},
),
],
);
},
)
Access to ReactterContext #
Reactter provides additional methods to BuildContext for access your ReactterContext. These are:
context.of<T>: Get theReactterContextinstance of the specified type and watch context's states or states defined on first parameter.
final watchContext = context.of<WatchContext>();
final watchHooksContext = context.of<WatchHooksContext>(
(ctx) => [ctx.stateA, ctx.stateB],
);
context.ofId<T>: Get theReactterContextinstance of the specified type and id defined on first parameter and watch context's states or states defined on second parameter.
final watchIdContext = context.ofId<WatchIdContext>('id');
final watchHooksIdContext = context.ofId<WatchHooksIdContext>(
'id',
(ctx) => [ctx.stateA, ctx.stateB],
);
context.ofStatic<T>: Get theReactterContextinstance of the specified type.
final readContext = context.ofStatic<ReadContext>();
context.ofIdStatic<T>: Get theReactterContextinstance of the specified type and id defined on first parameter.
final readIdContext = context.ofIdStatic<ReadIdContext>('id');
NOTE:
context.of<T>andcontext.ofId<T>watch all or some of the specifiedReactterHookdependencies and when it will change, re-render widgets in the scope ofReactterProviderorReactterBuilder.NOTE: These methods mentioned above uses
ReactterProvider.contextOf<T>
Lifecycle of ReactterContext #
ReactterContext provides lifecycle methods that are invoked in different stages of the instance’s existence.
class AppContext extends ReactterContext {
AppContext() {
print('1. Initialized');
onWillMount(() => print('2. Before mount'));
onDidMount(() => print('3. Mounted'));
onWillUpdate(() => print('4. Before update'));
onDidUpdate(() => print('5. Updated'));
onWillUnmount(() => print('6. Before unmounted'));
}
}
- Initialized: Class's constructor is the first one that is executed after the instance has been created.
onWillMount: Will trigger before theReactterContextinstance will mount in the tree byReactterProvider.onDidMount: Will trigger after theReactterContextinstance did mount in the tree byReactterProvider.onWillUpdate: Will trigger before theReactterContextinstance will update by anyReactterHook.onDidUpdate: Will trigger after theReactterContextinstance did update by anyReactterHook.onWillUnmount: Will trigger before theReactterContextinstance will unmount in the tree byReactterProvider.
NOTE:
UseContexthasonInitparameter which is execute between constructor andonWillMount, you can use to access to instance and putin data before mount.
Create a ReactterComponent #
ReactterComponent is a StatelessWidget class that wrap render with ReactterProvider and UseContext.
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}");
}
}
Using UseAsyncState hook #
UseAsyncState is a hook with the same functionality as UseState but providing a asyncValue which it will be obtain when execute resolve method.
This is a example:
class AppContext extends ReactterContext {
late final state = UseAsyncState<String?, Data>(null, _resolveState, this);
AppContext() {
_init();
}
Future<void> _init() async {
await state.resolve(Data(prop: true, prop2: "test"));
print("State resolved with: ${state.value}");
}
Future<String> _resolveState([Data arg]) async {
return await api.getState(arg.prop, arg.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 isDataclass.
UseAsyncState provides when method, which can be used for get a widget depending of it's state, like that:
ReactterProvider(
contexts: [
UseContext(() => AppContext()),
],
builder: (context, child) {
final appContext = context.of<AppContext>();
return appContext.state.when(
standby: (value) => Text("Standby: " + value),
loading: () => const CircularProgressIndicator(),
done: (value) => Text(value),
error: (error) => const Text(
"Ha ocurrido un error al completar la solicitud",
style: TextStyle(color: Colors.red),
),
);
},
)
Create a custom hook #
For create a custom hook, you should be create a class that extends of ReactterHook.
This is a 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
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,
);
}, [count], this);
}
}
Global state #
The reactter's hooks can be defined as static for access its as global way:
class Global {
static final flag = UseState(false);
static final count = UseCount(0);
// Create a class factory for 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: 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 instance it for can execute Global._init(This executes one time only).
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.
Resources #
Roadmap #
We want keeping adding features for Reactter, those are some we have in mind order by priority:
V3 #
- Async context.
- Make
Reacttereasy for debugging. - Structure proposal for large projects.
- Improve performance and do benchmark.
Contribute #
If you want to contribute don't hesitate to create an issue or pull-request in Reactter repository.
You can:
- Add a new custom hook.
- Add a new widget.
- Add examples.
- Provide new features.
- Report bugs.
- Report situations difficult to implement.
- Report an unclear error.
- Report unclear documentation.
- Write articles or make videos teaching how to use Reactter.
Any idea is welcome!
Authors #
- Leo Castellanos - [email protected]
- Carlos León - [email protected]