reactter 2.3.0-dev.3 copy "reactter: ^2.3.0-dev.3" to clipboard
reactter: ^2.3.0-dev.3 copied to clipboard

outdated

Reactter is a light, powerful and reactive state management.

Reactter


Pub Publisher Pub package Pub points MIT License

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 that Stream 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 with context argument, not need to add it on listenHooks, but is required declarate it as late.

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

NOTE: If you want to force notify, execute update method which UseState 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 to UseEffect, the callback don't execute on lifecycle willMount, and the cleanup don't execute on lifecycle willUnmount.

NOTE: If you want to execute a UseEffect immediately, use UseEffect.dispatchEffect instead of the context 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, use onInit method which UseContext exposes for access its instance and put the data you need.

NOTE: You can use id parameter of UseContext for create a different instance of same ReactterContext 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 the ReactterContext 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 the ReactterContext 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],
);
final readContext = context.read<ReadContext>();
  • context.readId<T>: Get the ReactterContext instance of the specified type and id defined on first parameter.
final readIdContext = context.readId<ReadIdContext>('id');

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

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'));
  }
}
  1. Initialized: Class's constructor is the first one that is executed after the instance has been created.
  2. onWillMount: Will trigger before the ReactterContext instance will mount in the tree by ReactterProvider.
  3. onDidMount: Will trigger after the ReactterContext instance did mount in the tree by ReactterProvider.
  4. onWillUpdate: Will trigger before the ReactterContext instance will update by any ReactterHook.
  5. onDidUpdate: Will trigger after the ReactterContext instance did update by any ReactterHook.
  6. onWillUnmount: Will trigger before the ReactterContext instance will unmount in the tree by ReactterProvider.

NOTE: UseContext has onInit parameter which is execute between constructor and onWillMount, 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 from resolve method. Like example shown above, which type argument send is Data 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 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,
      );
    }, [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!

Authors #

49
likes
0
pub points
4%
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

More

Packages that depend on reactter