reactter 3.2.0 copy "reactter: ^3.2.0" to clipboard
reactter: ^3.2.0 copied to clipboard

Reactter is a light, powerful and reactive state management.

Reactter


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

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.

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';

Usage #

reactter Concept Diagram

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 Context suffix, for easy locatily.

Lifecycle of ReactterContext #

ReactterContext has the following Lifecycle events:

  • Lifecycle.registered: Event when the instance has registered by ReactterInstanceManager.

  • Lifecycle.unregistered: Event when the instance has unregistered by ReactterInstanceManager.

  • Lifecycle.inicialized: Event when the instance has inicialized by ReactterInstanceManager.

  • Lifecycle.willMount: Event when the instance will be mount in the widget tree (it use with flutter_reactter only).

  • Lifecycle.didMount: Event when the instance did be mount in the widget tree (it use with flutter_reactter only).

  • Lifecycle.willUpdate: Event when any instance's hooks will be update. Event param is a ReactterHook.

  • Lifecycle.didUpdate: Event when any instance's hooks did be update. Event param is a ReactterHook.

  • Lifecycle.willUnmount: Event when the instance will be unmount in the widget tree(it use with flutter_reactter only).

  • Lifecycle.destroyed: Event when the instance did be destroyed by ReactterInstanceManager.

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 instance of Reactter that exposes some methods 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 creates 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>('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 through UseContext.

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);
  // 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 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>).

Using UseState hook #

UseState is a ReactterHook that manages a state.

You can add it on any part of class, with the context argument(this) to put this hook on listen:

class AppContext extends ReactterContext {
  late final count = UseState(0, this);
}

or add it into listenHooks method which is exposed by ReactterContext:

class AppContext extends ReactterContext {
  final count = UseState(0);

  AppContext() {
    listenHooks([count]);
  }
}

NOTE: If you don't add context argument or use listenHook, the ReactterContext 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 a Object, not detect internal changes, only when states is setted another Object.

If you want to force notify, execute update method, which is exposed by UseState.

class Todo {
  String name;

  Todo(this.name);
}

class AppContext extends ReactterContext {
  final todoState = UseState(Todo('Do this'), this);

  AppContext() {
    todoState.update(() {
      todoState.value.name = 'Do this other';
    });
  }
}

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,
    this,
  );

  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 through resolve method. Like the example shown above, the argument type send is TranslateArgs 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 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 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 the ReactterContext 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"),
  [someHook],
  UseEffect.dispatchEffect
);

NOTE: If you don't add context 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).

Create a 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 we 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 provides update method which notify to context 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);
  }
}

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(() {
      Future.delayed(const Duration(seconds: 1), changeCount);
    }, [count], UseEffect.dispatchEffect);
  }

  static void changeCount() {
    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 #

flutter_reactter Concept Diagram

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 use onInit method to access its instance and put the data you need.

NOTE: ReactteProvider is a "scoped". So it contains a ReactterScope which the builder callback will be rebuild, when the ReactterContext changes. For this to happen, the ReactterContext should put it on listens for BuildContext's watchers.

Access to ReactterContext #

Reactter provides additional methods to BuildContext to access your ReactterContext. These are following:

  • context.watch: Gets the ReactterContext's instance from the closest ancestor of ReactterProvider and watch all ReactterHook or ReactterHook defined in first paramater.
final watchContext = context.watch<WatchContext>();
final watchHooksContext = context.watch<WatchHooksContext>(
  (ctx) => [ctx.stateA, ctx.stateB],
);
  • context.watchId: Gets the ReactterContext's instance with id from the closest ancestor of ReactterProvider and watch all ReactterHook or ReactterHook defined in second paramater.
final watchIdContext = context.watchId<WatchIdContext>('id');
final watchHooksIdContext = context.watchId<WatchHooksIdContext>(
  'id',
  (ctx) => [ctx.stateA, ctx.stateB],
);
  • context.use: Gets the ReactterContext's instance with/without id from the closest ancestor of ReactterProvider.
final readContext = context.use<ReadContext>();
final readIdContext = context.use<ReadIdContext>('id');

NOTE: These methods mentioned above uses ReactterProvider.contextOf

NOTE: context.watch and context.watchId watch all or some of the specified ReactterHook dependencies and when it will change, re-render widgets in the scope of ReactterProviders, ReactterBuilder or ReactterScope.

Control re-render with ReactterScope #

ReactterScope is a wrapeer StatelessWidget that to 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 builder callback will be rebuild, when the ReactterContext changes. For this to happen, the ReactterContext should put it on listens for BuildContext's watchers.

Control re-render with ReactterBuilder #

ReactterBuilder is a wrapper 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>(
  listenAllHooks: true,
  builder: (appContext, context, child) {
    return Text("Count: ${appContext.count.value}");
  },
)

NOTE: ReactterBuilder is read-only by default(listenAllHooks: false), this means it only renders once. Instead use listenAllHooks as true or use listenHooks with the ReactterHooks specific and then the builder callback will be rebuild with every ReactterContext's ReactterHook changes.

NOTE: ReactterBuilder is a "scoped". So it contains a ReactterScope which the builder callback will be rebuild, when the ReactterContext changes. For this to happen, the ReactterContext should put it on listens for BuildContext's watchers.

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 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!

Authors #

48
likes
140
pub points
3%
popularity

Publisher

verified publisher2devs.io

Reactter is a light, powerful and reactive state management.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (LICENSE)

More

Packages that depend on reactter