Reactter


Pub Publisher Reactter Flutter Reactter Pub points MIT License GitHub Workflow Status Codecov

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.
  • ๐Ÿ’ง Adaptable to any architecture.
  • โ˜ข๏ธ Reactive state using Signal.
  • โ™ป๏ธ Reuse state creating custom hooks.
  • ๐Ÿช„ No configuration and no code generation necessary.
  • ๐ŸŽฎ Total control to re-render widget tree.
  • ๐Ÿ’™ 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

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โ€‚ Reactter

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โ€‚ Flutter Reactter

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 Reactter Lint which will help to encourage good coding practices and preventing 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 extendsReactterState, 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 prefix Use according to convention.

RECOMMENDED: See also different 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 strSignal = "initial value".signal;
final intSignal = 0.signal;
final userSignal = User().signal;

or using the constructor class Signal<Type>(InitialValue):

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 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 its value has changed when the previous value is different from the current value. If its value is a Object, not detect internal changes, only when value is setted another Object.

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 is changed, the Signal will emitted the following events(learn about it here):

  • Lifecycle.willUpdate event is triggered before the value change or update, refresh methods have been invoked.
  • Lifecicle.didUpdate event is triggered after the value change or update, refresh methods have been invoked.

NOTE: When you do any arithmetic operation between two Signals, its return a Obj, for example: 1.signal + 2.signal return 3.obj. A Obj is like a Signal without reactive functionality, but you can convert it to Signal using .toSignal.

NOTE: In flutter, using ReactterWatcher, it's 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.

It can be declared inside a class, like this:

class CounterController {
  final count = UseState(0);
}

UseState has 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 its value has changed when the previous value is different from the current value. If its value is a Object, not detect internal changes, only when value is setted another Object.

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 is changed, the UseState will emitted the following events(learn about it here):

  • Lifecycle.willUpdate event is triggered before the value change or update, refresh methods have been invoked.
  • Lifecicle.didUpdate event is triggered after the value change or update, refresh methods have been invoked.

Different between Signal and UseState

Both UseState and Signal represent a state(ReactterState). But there are a few featues 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.

UseAsyncState

UseAsyncState is a hook(ReactterHook) with the same feature as UseState but provides a asyncValue method, which will be obtained when resolve method is executed.

This is an translate example:

class TranslateArgs {
  final String text;
  final String from;
  final String to;

  TranslateArgs({ this.text, this.from, this.to, });
}

class TranslateController {
  late final translateState = UseAsyncStates<String?, TranslateArgs>(
    null,
    translate
  );

  TranslateController() {
    translateState.resolve(
      TranslateArgs({
        text: 'Hello world',
        from: 'EN',
        to: 'ES',
      }),
    ).then((_) {
      print("'Hello world' translated to Spanish: '${translateState.value}'");
    });
  }

  Future<String> translate([TranslateArgs args]) async {
    // this is fake code, which simulates a request to API
    return await api.translate(args);
  }
}

NOTE: If you want to send argument to asyncValue method, need to define a type argument which be send through resolve method. Like the example shown above, the argument type send is TranslateArgs class.

Use when method to returns 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 is changed, the UseAsynState will emitted the following events(learn about it here):

  • Lifecycle.willUpdate event is triggered before the value change or update, refresh methods have been invoked.
  • Lifecicle.didUpdate event is triggered after the value change or update, 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 to UseState 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(<reducer>, <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 with which you initialize the 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 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 is changed, the UseReducer will emitted the following events(learn about it here):

  • Lifecycle.willUpdate event is triggered before the value change or update, refresh methods have been invoked.
  • Lifecicle.didUpdate event is triggered after the value change or update, refresh methods have been invoked.

UseCompute

UseCompute is a hook(ReactterHook) that keeps listening for state dependencies changes, to return a computed value from a defined method.

An example is shown below:

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 value property which represent the computed value.

NOTE: UseCompute notifies that its value has changed when the previous value is different from the current value.

When value is changed, the UseState will emitted the following events(learn about it here):

  • Lifecycle.willUpdate event is triggered before the value change or update, refresh methods have been invoked.
  • Lifecicle.didUpdate event is triggered after the value change or update, refresh methods have been invoked.

NOTE: UseCompute is read-only, it's mean that its value cannot be changed, except by invoking the UseCompute method.

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: For creating a ReactterHook, it must be registered by adding the following line: final $ = ReactterHook.$register;

NOTE: ReactterHook provides update method which notifies about its changed.

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}",
    );
  }
}

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 a builder function, enabling the creation of an instance using Reactter.get.

    Reactter.register(builder: () => AppController());
    Reactter.register(id: "uniqueId", builder: () => AppController());
    
  • Reactter.unregister: This method removes the builder 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 the build registered with reactter.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 keep the builder function.

    Reactter.delete<AppController>();
    Reactter.delete<AppController>('uniqueId');
    

NOTE: The scope of the registered instances is global. This indicates that using Reactter.get or UseContext 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 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 access 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 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.

RECOMMENDED: Dont's use Object with constructor parameters to prevent conflicts.

NOTE: ReactteProvider is a "scoped". So, the builder method will be rebuild, when the instance changes or any ReactterState specified using the watch methods of BuildContext extension.

ReactterProviders

ReactterProviders is a Widget (exclusive of flutter_reactter) that allows to use multiple ReactterProvider as 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: Dont's use Object with constructor parameters to prevent conflicts.

NOTE: ReactteProviders is a "scoped". So, the builder method will be rebuild, when the instance changes or any ReactterState specified using the watch methods of BuildContext extension.

ReactterComponent

ReactterComponent is a Widget (exclusive of flutter_reactter) that provides ReactterProvider features, whose T instance defined is exposing 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 T instance is not created and instead tried to be found it in the nearest ancestor where it was created.

RECOMMENDED: Dont's 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 render method.

Use listenAll getter as true to listen all the T instance changes to rebuild the Widget tree defined in 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 lifecycle's 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 of flutter_reactter) happens when the instance is going to be mounted in the widget tree.
  • Lifecycle.didMount: This event(exclusive of flutter_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 a ReactterState.
  • Lifecycle.didUpdate: This event is triggered anytime the instance's state has been updated. The event parameter is a ReactterState.
  • Lifecycle.willUnmount: This event(exclusive of flutter_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

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, the callback 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, the callback 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 the callback 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 the param 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 the param 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 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 off method to stop listening instance's event:

UseEvent<AppController>().off(Events.SomeEvent, onSomeEvent);

Use one method to listen for instance's event once:

UseEvent<AppController>().one(Events.SomeEvent, onSomeEvent);

Use emit method to trigger a instance's event:

UseEvent<AppController>().emit(Events.SomeEvent, 'Parameter');

IMPORTANT: Don't forget to remove event using off or using dispose 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.

UseEffect

UseEffect is a hook(ReactterHook) that allows to manager side-effect.

UseEffect(
  <<Function cleanup> Function callback>,
  <ReactterState dependencies>[],
  <Object instance>,
)

The side-effect logic into the callback function is executed when dependencies(List of ReactterState) argument changes or instance(Object) 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(Object) 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 instance argument to UseEffect, the callback don't execute on lifecycle didMount, and the cleanup don't execute on lifecycle willUnmount (theses LifeCycle events are used with flutter_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 Signals 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 the T instance from ReactterProvider's nearest ancestor and listens any instance changes or ReactterState 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 the T instance by id from ReactterProvider's nearest ancestor and listens instance changes or ReactterState 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 the T instance with/without id from ReactterProvider'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 and context.watchId watch all or some of the specified ReactterState dependencies, when any it will changes, re-built the Widgets tree in the scope of ReactterProvider, ReactterComponent or any Widget that exposes the BuildContext like Build, StatelessWidget, StatefulWidget.

NOTE: A ReactterState can be a Signal or ReactterHook (like UseState, UseAsynState, UseReducer or another Custom hooks).

Resources

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!

Authors

Libraries

flutter_reactter