reactter 2.3.0-dev.3 reactter: ^2.3.0-dev.3 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
Context
suffix, 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 thatStream
consumes 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
UseState
withcontext
argument, 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
update
method whichUseState
exposes 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
context
argument toUseEffect
, thecallback
don't execute on lifecyclewillMount
, and thecleanup
don't execute on lifecyclewillUnmount
.NOTE: If you want to execute a
UseEffect
immediately, useUseEffect.dispatchEffect
instead of thecontext
argument.
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.watch<AppContext>();
final appConfigContext = context.watchId<ConfigContext>('App');
final userConfigContext = context.watchId<ConfigContext>('User');
return [...]
},
);
}
}
For more information about context.watch[...]
go to here.
RECOMMENDED: Don't use
ReactterContext
class with parameters to prevent conflicts. Instead of it, useonInit
method whichUseContext
exposes for access its instance and put the data you need.NOTE: You can use
id
parameter ofUseContext
for create a different instance of sameReactterContext
class.
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.watch<T> or context.watchId<T> here,
// it forces re-render this builder together ReactterBuilder's builder.
final appContext = context.read<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.watch<T>
: Get theReactterContext
instance of the specified type and watch context's states or states defined on first parameter.
final watchContext = context.watch<WatchContext>();
final watchHooksContext = context.watch<WatchHooksContext>(
(ctx) => [ctx.stateA, ctx.stateB],
);
context.watchId<T>
: Get theReactterContext
instance of the specified type and id defined on first parameter and watch context's states or states defined on second parameter.
final watchIdContext = context.watchId<WatchIdContext>('id');
final watchHooksIdContext = context.watchId<WatchHooksIdContext>(
'id',
(ctx) => [ctx.stateA, ctx.stateB],
);
context.read<T>
: Get theReactterContext
instance of the specified type.
final readContext = context.read<ReadContext>();
context.readId<T>
: Get theReactterContext
instance of the specified type and id defined on first parameter.
final readIdContext = context.readId<ReadIdContext>('id');
NOTE:
context.watch<T>
andcontext.watchId<T>
watch all or some of the specifiedReactterHook
dependencies and when it will change, re-render widgets in the scope ofReactterProvider
orReactterBuilder
.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 theReactterContext
instance will mount in the tree byReactterProvider
.onDidMount
: Will trigger after theReactterContext
instance did mount in the tree byReactterProvider
.onWillUpdate
: Will trigger before theReactterContext
instance will update by anyReactterHook
.onDidUpdate
: Will trigger after theReactterContext
instance did update by anyReactterHook
.onWillUnmount
: Will trigger before theReactterContext
instance will unmount in the tree byReactterProvider
.
NOTE:
UseContext
hasonInit
parameter 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
asyncValue
method, need to defined a type arg which its send fromresolve
method. Like example shown above, which type argument send isData
class.
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.watch<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
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(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
Reactter
easy 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!