Skip to content
GIfatahTH edited this page Jan 5, 2022 · 20 revisions

Table of Contents

Overview:

To mutate the state of an RM.injected, we can:

  • For immutable state:
model.state = newState;
  • For state of type bool:
model.toggle();
// Equivalents to:
model.state = !model.state;
  • For any kind of state, whether immutable or mutable:
model.setState(
    (T state) => ....,
);

Use Case:

  • Basic:
ElevatedButton(
  child: const Text('🏎️ Counter1 ++'),
  onPressed: () => model.setState((s)=> s++),
),

// NOTE: Mutation for part of the state, such as map, class object
final mapModel = {}.inj<String, String>();
final classModel = User(name: 'Joe Bryden').inj();

//⛔ DON'T, it won't work
mapModel.setState((s) => s['key'] = newValue);
classModel.setState((s) => s.name = 'Sleepy J');

//👍 DO
mapModel.setState((s) { s['key'] = newValue; });
classModel.setState((s) { s.name = 'Joe Biden' });
classModel.setState((s) => s..name = 'Joe Biden');

Why ⛔ DON'T? Regarding this issues.

  • Dealing with Async Future or Stream:

Methods can return stream, future, or simply sync objects, states_rebuilder treats them all equally.

// setState from future, it'll notify the Builders to rebuild into a `onWaiting` widget
// then finally it's `onData` or `onError`.
ElevatedButton(
  child: const Text('🏎️ Counter1 ++'),
  onPressed: () => model.setState((s)=> s.fetchToUpdateState()),
),

After the state is mutated, state listeners are notified to rebuild.

setState is also used for more options for state mutation. This is the setState API:

Future<T?> setState(
    dynamic Function(T)? fn, {
    SideEffects? sideEffects,
    int debounceDelay = 0, 
    int throttleDelay = 0, 
    bool shouldAwait = false, 
    bool skipWaiting = false, 
    BuildContext? context
  }
)

Definition of Parameters:

state mutation callback (fn)

The positional parameter is a callback that will be invoked to mutate the state. It exposes the current state. It can have ant type of return including Future and Stream.

While calling the mutation callback, states_rebuilder checks the return type:

  • If it is a sync object, the state is mutated and notification is emitted with onData status flag.
  • If the return type is Future, a notification is emitted with isWaiting status flag, and the state waits for the result of the Future and once data is obtained, a notification is emitted with onData status flag.
  • If the return is Stream, a notification is emitted with isWaiting status flag, and the state subscribe to the stream, and emits a notification with onData status flag each time a data is emitted.
  • If an error is caught, a notification is emitted with hasError status flag and the error object.

stateInterceptor

stateInterceptor is called after new state calculation and just before state mutation and notification. It exposes the current and the next snapState.

You can ignore state mutation by returning the current snapState.

Using stateInterceptor we can ignore the waiting or the error phase which is very useful in some practical use cases.

Here is an example where the waiting and error states are ignored

return _counterState.setState(
      (s) => myRepository.state.incrementAsync(counter),
      //
      // stateInterceptor is called after next state calculation, and just before
      // state mutation. It exposes the current and the next snapState
      stateInterceptor: (currentSnap, nextSnap) {
        // if the next state will be the waiting or the error state,
        // just return the current sate
        if (!nextSnap.hasData) return currentSnap;
        //
        // This is the right place to do some state validation before mutation
      },
    );

Here is another use case where we add an item optimistically.

 void addTodo(String description) {
    // cache the todo to add
    final todoToAdd = Todo(
      description: description,
      // we have the id form the backend
      id: DateTime.now().millisecondsSinceEpoch.toString(),
    );
    _todosRM.setState(
      (s) async* {
        // use stream
        //
        // yield the updated state
        yield [
          ..._todosRM.state,
          todoToAdd,
        ];
        // call the server to add the todo
        // we are optimistic and we expect to add it without problem
        await repository.addTodo(todoToAdd);
      },
      stateInterceptor: (current, next) {
        // skip the waiting state
        if (next.isWaiting) return current;
        if (next.hasError) {
          // if the server fails to add the todo
          // just return the last state before update.
          return next.copyWith(
            data: next.state.where((todo) => todo.id != todoToAdd.id).toList(),
          );
        }
      },
    );
  }

sideEffects

Injected state, can be defined to handle side effect the time of injecting it. sideEffects is called after notification emission and before widget rebuild. example:

final model = RM.inject(
    ()=> Model(),
    sideEffects: SideEffects.onError(
        (err, refresh)=> //show snack bar
    )
)

The sideEffects defined like this is considered as the default one. And when setState is called without defining its sideEffects parameter, the latter sideEffects will be invoked.

In some scenarios, one might want to define different side effects for a particular call of setState.

The sideEffects defined in setState method, will override the default sideEffects defined in RM.injected if shouldOverrideDefaultSideEffects return true.

The SideEffects class has other named constructors:

// Called when notified regardless of state status of the notification
SideEffects(
  onSetState: (snapState)=> print('$snapState'),
  onAfterBuild: ()=> print('onAfterBuild'),
);
// Called when notified with data status
SideEffects.onData((data)=> print('data'));
// Called when notified with waiting status
SideEffects.onWaiting(()=> print('waiting'));
// Called when notified with error status
SideEffects.onError((error, refresh)=> print('error'));
// Exhaustively handle all four status
SideEffects.onAll(
 onIdle: ()=> print('Idle'), // If is Idle
 onWaiting: ()=> print('Waiting'), // If is waiting
 onError: (err, refresh)=> print('Error'), // If has error 
 onData: (data)=> print('Data'), // If has Data
)
// Optionally handle the four status
SideEffects.onOrElse(
 onWaiting: ()=> print('Waiting'),
 onError: (err, refresh)=> print('Error'),
 onData: (data)=> print('Data'),
 orElse: (data) => print('orElse')
)

Note that setState side effects will not override the default ones unless shouldOverrideDefaultSideEffects return true. Example:

final model = RM.inject(
 ()=> Model(),
  sideEffects: SideEffects.onWaiting(
    () => print('Show snack bar…'),
  ),
 )
)
// The call of setState without onWaiting handling will invoke the default onWaiting callback (showing a snack bar)
model.setState((s)=> …);
//But if we want to show a dialog instead of snackbar for this particular call of setState, we override the default side effects defined in `RM.inject` using shouldOverrideDefaultSideEffects
model.setState(
 (s)=> ..,
 sideEffects: SideEffects.onWaiting(
    () => print('Show dialog'),
  ),
 shouldOverrideDefaultSideEffects: (snap)=> snap.isWaiting,
 )
)

debounceDelay:

The time in milliseconds that should be passed without and other call of setState to execute the mutation callback.

throttleDelay:

The time in milliseconds within only one call of the mutation callback is allowed.

shouldAwait

If set to true and if the state is waiting for an async task, the mutation callback is only after the async task ends with data.

skipWaiting

If set to true, the isWaiting and onWaiting state status are ignored.

context

Used to define the BuildContext to be used for side effects executed after this call of setState. In most cases you do not need to define it because BuildContext is available using RM.context.