Skip to content
GIfatahTH edited this page Oct 4, 2021 · 10 revisions

Table of Contents

Overview of APIs:

Injected<T> RM.inject<T>(
    T Function() creator, {
        T? initialState, 
        SideEffects? sideEffects,
        void Function(T? s)? onInitialized,
        SnapState<T> Function(SnapState<T> current, SnapState<T> next ) stateInterceptor,
        DependsOn<T>? dependsOn, 
        int undoStackLength = 0, 
        PersistState<T> Function()? persist, 
        bool autoDisposeWhenNotUsed = true, 
        bool isLazy = true, 
        String? debugPrintWhenNotifiedPreMessage
    }
)

//Similar to RM.inject, except that the creator returns Future<T>
Injected<T> RM.injectFuture<T>(
    Future<T> Function() creator, {
        T? initialState, 
        void Function(T? s)? onInitialized,
        SnapState<T> Function(SnapState<T> current, SnapState<T> next ) stateInterceptor,, 
        DependsOn<T>? dependsOn, 
        int undoStackLength = 0, 
        PersistState<T> Function()? persist, 
        bool autoDisposeWhenNotUsed = true, 
        bool isLazy = true, 
        String? debugPrintWhenNotifiedPreMessage
    }
)

//Similar to RM.inject, except that the creator returns Stream<T> and
//the onInitialized exposes the current StreamSubscription
Injected<T> RM.injectStream<T>(
    Stream<T> Function() creator, {
        T? initialState, 
        void Function(T, StreamSubscription)? onInitialized, 
        SnapState<T> Function(SnapState<T> current, SnapState<T> next ) stateInterceptor,
        DependsOn<T>? dependsOn, 
        int undoStackLength = 0, 
        PersistState<T> Function()? persist, 
        bool autoDisposeWhenNotUsed = true, 
        bool isLazy = true, 
        String? debugPrintWhenNotifiedPreMessage
    }
)

//Similar to RM.inject, except that the creator is a Map<dynamic, FutureOr<T> Function()> 
Injected<T> RM.injectFlavor<T>(
    Map<dynamic, FutureOr<T> Function()>  creator, {
        T? initialState, 
        void Function(T? s)? onInitialized,
        SnapState<T> Function(SnapState<T> current, SnapState<T> next ) stateInterceptor, 
        DependsOn<T>? dependsOn, 
        int undoStackLength = 0, 
        PersistState<T> Function()? persist, 
        bool autoDisposeWhenNotUsed = true, 
        bool isLazy = true, 
        String? debugPrintWhenNotifiedPreMessage
    }
)

RM.inject is used to inject primitives, enum or objects. Example:

final counter = RM.inject<int>(()=> 0);
final model = RM.inject<Model>(()=> Model());
//For simple injection, you can use extensions
final counter = 0.inj();
final switcher = false.inj();
final model = Model().inj<Model>();

Definition of Parameters:

creator

The creator parameter is a callback the is used to create the injected state. When refresh method is invoked on the injected state, the creator callback is re-executed to define the new state.

The creator callback is called lazily, that is, it will not be invoked at the time of the instantiating of the Injected state, but it will be called the first time the state is used. (see isLazy parameter below).

initialState

Because of the null safety, the state can not be null. For this reason, the initial state before calling the creator is inferred by the library as follows 

  • If the state is int the initial state is 0.
  • If the state is double the initial state is 0.0.
  • If the Sting is double the initial state is empty String.
  • If the state is bool the initial state is false.
  • If the state is not primitive the initial state is the first created instance. Example:
final counter = RM.inject(()=> 10); // initialState is 0.
final model = RM.inject(()=> Model()); // the initial state is the instance created after invoking creator callback.

You have the choice to define the initial state of your choice using the initialState parameter. For RM.injectFuture and RM.injectStream where the state is not defined synchronically. If the state is primitive then it is inferred by the library, in the other case the state can not be defined until the async task emits data. In this case you have either to define the initial state explicitly or to handle the waiting state status and not getting the state until is ready.

If you try to get the state when it is not ready, ArgumentError.notNull is thrown

sideEffects

To handle side effects when the state is mutated and emits notification, you use sideEffects parameter that takes an SideEffects object: example:

final model = RM.inject(
 ()=> Model(),
 sideEffects: SideEffects.onAll(
   onIdle: ()=> print('Idle'),
   onWaiting: ()=> print('Waiting…'),
   onError: (err, refresh) => print ('Error'),
   onData: (data) => // Navigate to …
 )
)

The SideEffects class has other named constructors:

// Called when notified regardless of state status of the notification
SideEffects(
  initState: ()=> print('initState'),
  onSetState: (snapState)=> print('$snapState'),
  onAfterBuild: ()=> print('onAfterBuild'),
  dispose: ()=> print('dispose'),
);
// 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 side effects defined here are the default side effects, they can be overridden for a particular call of SideEffects method. See setState API. 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,
 )
)

onInitialized

Optional callback That exposed the state Callback to be executed after the injected model is first created. It is similar to [SideEffects.initState] except that it exposes the state for some useful cases.

It can be used to pause the subscription after initialization. Example:

final stream = RM.injectStream<int>(
 ()=> Stream.periodic(Duration(seconds:1), (val)=> val),
 onInitialized: (state, subscription){
 //Pausing the subscription
 subscription.pause();
 }
);
//later on, we can restart the stream;
stream.subscription.start();

stateInterceptor

This is a callback that is used to track the state's life and transitions. It exposes the state before state calculation (called currentSnap) and a snap of the state after new calculation and just before state mutation.

final model = RM.inject(
    ()=> Model(),
    stateInterceptor: (currentSnap, nextSnap) {
        print(currentSnap); //snap state before calculation
        print(nextSnap); //snap state after calculation
        //
        if(nextSnap.hasError) {
            //Error and stackTraces can be sent to a an error tracking service
            print(nextSnap.error);
            print(nextSnap.stackTrace);
        }

        ///If return nothing or return null, the state will be mutate to hold the nextSnap
    },
)

For more information about the build-in logger see here

Inside the stateInterceptor callback, you can change how the state will be mutated. Here is an example of a count-down timer that counts from 60 to 0 seconds

final timer = RM.injectStream<int>(
  //stream emits 0, 1, 2, ... infinitely
  () => Stream.periodic(Duration(seconds: 1), (tick) => tick + 1),
  stateInterceptor: (currentSnap, nextSnap) {
    if (nextSnap.data > 60) {
      //cancel subscription and return the currentSnap (holds 0)
      timer.subscription.cancel();
      return currentSnap;
    }
    return nextSnap.copyToHasData(
      // It will return a state of 60, 59, 58, ... 0
      60 - nextSnap.data,
    );
  },
)

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: OnReactive(
            () => Text(timer.state.toString()),
          ),
        ),
      ),
    );
  }
}

This is another example of email field validation

final email = RM.inject<String>(
  () => '',
  stateInterceptor: (currentSnap, nextSnap) {
    if (nextSnap.hasData) {
      if (!nextSnap.data.contains('@')) {
        return nextSnap.copyToHasError(
          Exception('Enter a valid Email'),
        );
      }
    }
  },
);

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: OnReactive(
          () => TextField(
            onChanged: (value) => email.state = value,
            decoration: InputDecoration(
              errorText: email.error?.message,
            ),
          ),
        ),
      ),
    );
  }
}

dependsOn

An injected state can depend on one or more other injected states. Example:

final stateA = RM.inject(()=> StateA());
final stateB = RM.injectFuture(()=> StateB().init());
final stateC = RM.inject(
 ()=> StateC( stateA: stateA, stateB: stateB),
 dependsOn: DependsOn(
 {stateA, stateB},
 //Option param
 shouldNotify: (stateC)=> someCondition
 debounceDelay:500 // in milliseconds
 throttleDelay: 500 // in milliseconds
 )
)

The stateC depends on stateA and stateB. This means that when any of stateA and stateB emits notification, the creator callback of stateC is re-invoked to calculate the new state and notify its listeners. The state status of stateC is a combination of both state status of stateA and stateC:

  • If stateA isWaiting and/or stateB isWaiting than stateC isWaiting;
  • If stateA hasError and/or stateB hasError than stateC hasError;
  • If stateA asData and stateB hasData than stateC hasData; Optionally, stateC recalculation can be:
  • stopped if shouldNotify callback return false.
  • debounced with the time defined with debounceDelay parameter.
  • throttled with the time defined with throttleDelay parameter.

undoStackLength

If undoStackLength is defined, the state can be undone and/or redone.

  • To check if the state can be undone use model.canUndoState, and to undo it use model.undoState()
  • To check if the state can be redone use model.canRedoState, and to undo it use model.redoState()

persist

To be able to persist the state you have first to implement the IPersistStore interface with a local storage provider of your choice. This is an example of hive plugging:

class HiveImp implements IPersistStore {
 Box box;
@override
 Future<void> init() async {
 await Hive.initFlutter();
 box = await Hive.openBox('myBox');
 }
@override
 Object read(String key) {
 return box.get(key);
 }
@override
 Future<void> write<T>(String key, T value) async {
 return box.put(key, value);
 }
@override
 Future<void> delete(String key) async {
 return box.delete(key);
 }
@override
 Future<void> deleteAll() async {
 return box.clear();
 }
}

The next step is to initialize the storage provider. In the main method:

 void main()async{
 WidgetsFlutterBinding.ensureInitialized();
 
 await RM.storageInitializer(HiveImp());
 runApp(MyApp());
 }

Now when injecting the state, we can set its storage setting using the persist parameter:

 final model = RM.inject<MyModel>(
   ()=>MyModel(),
   persist:() => PersistState(
   key: 'modelKey',
   toJson: (MyModel s) => s.toJson(),
   fromJson: (String json) => MyModel.fromJson(json),
   //Optionally, throttle the state persistance
   throttleDelay: 1000,
   //You can override the default storage provider
   persistStateProvider: AnotherPersistStateProvider()
   ),
 );

toJson is a callback that exposes the current state and returns a String representation of the state. If it is not defined, it will be inferred for primitive:

  • int: (int s)=> '$s';
  • double: (double s)=> '$s';
  • String: (String s)=> '$s';
  • bool: (bool s)=> s? '1' : '0';   If it is not defined and the model is not primitive, it will throw and ArgumentError. fromJson is a callback that exposes the String representation of the state and returns the parsed state. If it is not defined, it will be inferred for primitive:
  • int: (String json)=> int.parse(json);
  • double: (String json)=> double.parse(json);
  • String: (String json)=> json;
  • bool: (String json)=> json =='1'; If it is not defined and the model is not primitive, it will throw and ArgumentError. persistStateProvider if not defined the default storage provider initialized in the main method will be used.

autoDisposeWhenNotUsed

state is automatically disposed if no longer listen. If you want the state to be alive even if not used, you set autoDisposeWhenNotUsed to false. You can manually dispose the state invoking : model.dispose();

isLazy

state is lazily initialized; it will not initialized until first used. If this is not the behavior you want to set isLazy to false.

debugPrintWhenNotifiedPreMessage

To help you track the lifecycle of the state and debug your app, you can print an informative message to inform you when the state is initialized, notified and disposed.