flutter_reactter 3.0.0-dev.3 copy "flutter_reactter: ^3.0.0-dev.3" to clipboard
flutter_reactter: ^3.0.0-dev.3 copied to clipboard

Reactter is a light, powerful and reactive state management.

Reactter


Pub Publisher Reactter Flutter Reactter Pub points MIT License

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 Reactter
Flutter Flutter Reactter

Once you know what package you want to install, proceed to add the package on your project:

  • With command:

    dart pub add reactter
    

    For flutter:

    flutter pub add flutter_reactter
    
  • Or add a line like this into your pubspec.yaml file:

      dependencies:
        reactter: #add version here
    

    and then run dart pub get.

    For flutter:

      dependencies:
        flutter_reactter: #add version here
    

    and 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 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 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 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 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 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: 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, the ReactterContext won'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: 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 another Object.

NOTE: If you want to force notify, execute update method which UseState exposes 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 asyncValue method, need to defined a type arg which its send from resolve method. Like example shown above, which type argument send is Arguments class.

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

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(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 #

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: Don'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 witch 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.read: Gets the ReactterContext's instance from the closest ancestor of ReactterProvider.
final readContext = context.read<ReadContext>();
  • context.readId<T>: Gets the ReactterContext's instance with id from the closest ancestor of ReactterProvider
final readIdContext = context.readId<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 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 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 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: 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 witch 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 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 #

37
likes
0
pub points
35%
popularity

Publisher

verified publisher2devs.io

Reactter is a light, powerful and reactive state management.

Homepage
Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter, reactter

More

Packages that depend on flutter_reactter