GIfatahTH / states_rebuilder

a simple yet powerful state management technique for Flutter
495 stars 56 forks source link

On widget: improve readability by making listenTo the first positional argument #205

Closed samuelstroschein closed 3 years ago

samuelstroschein commented 3 years ago

Another day, another suggestion.

introduction

Ever since V4 of states rebuilder the On(() => child) widget is an an alternative to the previous fooState.rebuilder(() => child) syntax. Although the On widget (syntax) seems more appropriate in a widget tree because of the syntax highlighting, it has one major downside why I sometimes don't use it.

// the new syntax
On(() => 
  Text('example'),
).listenTo(fooState)

// the old syntax
fooState.rebuilder(
  () => Text('example'),
)

the "problem"

The On widget moves rebuilding logic at the end of the widget, opposite to the previous function syntax. That becomes problematic the longer a widget tree becomes:

example 1: relatively small widget tree

// the new syntax moves the information which state triggers rebuilds at the end
On(
  () {
    return TextField(
      onChanged: (String value) => password.state = Password(value),
      decoration: InputDecoration(
        hintText: "Password should be more than three characters",
        labelText: 'Password',
        errorText: password.error?.message,
      ),
    );
  },
).listenTo(password),

// immeditately obvious what triggers rebuilds. the password state
password.rebuilder(
  () {
    return TextField(
      onChanged: (String value) => password.state = Password(value),
      decoration: InputDecoration(
        hintText: "Password should be more than three characters",
        labelText: 'Password',
        errorText: password.error?.message,
      ),
    );
  },
)
example 2: bigger widget tree.

```Dart // keep in mind that this example is still simple because the on widget is the most outer widget and it's // relatively easy to see that the user state is listened to. in production code, that is most likely no the case // and the listenTo function is nested deeply in a widget tree. On( () { bool isLoading = user.isWaiting; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: SizedBox( child: isLoading ? CircularProgressIndicator() : Text( 'Sign In', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 30, ), ), height: 40.0, ), ), SizedBox(height: 32), //NOTE2: IF can log with apple if (canSignInWithApple.state) ...[ ElevatedButton( child: Text('Sign in With Apple Account'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.withApple), ), ), SizedBox(height: 8), ], ElevatedButton( child: Text('Sign in With Google Account'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.withGoogle), ), ), SizedBox(height: 8), ElevatedButton( child: Text('Sign in With Email and password'), onPressed: isLoading ? null : () { Navigator.push( context, MaterialPageRoute( builder: (context) { //Display form screen return SignInRegisterFormPage(); }, ), ); }, ), SizedBox(height: 8), ElevatedButton( child: Text('Sign in anonymously'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.anonymously), ), ), SizedBox(height: 8), ], ); }, ).listenTo(user), // immediately obvious what triggers rebuilds. the password state user.rebuilder( () { bool isLoading = user.isWaiting; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: SizedBox( child: isLoading ? CircularProgressIndicator() : Text( 'Sign In', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 30, ), ), height: 40.0, ), ), SizedBox(height: 32), //NOTE2: IF can log with apple if (canSignInWithApple.state) ...[ ElevatedButton( child: Text('Sign in With Apple Account'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.withApple), ), ), SizedBox(height: 8), ], ElevatedButton( child: Text('Sign in With Google Account'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.withGoogle), ), ), SizedBox(height: 8), ElevatedButton( child: Text('Sign in With Email and password'), onPressed: isLoading ? null : () { Navigator.push( context, MaterialPageRoute( builder: (context) { //Display form screen return SignInRegisterFormPage(); }, ), ); }, ), SizedBox(height: 8), ElevatedButton( child: Text('Sign in anonymously'), onPressed: isLoading ? null : () => user.auth.signIn( (_) => UserParam(signIn: SignIn.anonymously), ), ), SizedBox(height: 8), ], ); }, ) ```

suggestion

listenTo should be implemented in the On widget itself as first positional argument. The widget that is rebuilt should probably be given to the child parameter to further align with Flutter widget tree syntax.

// shouldRebuild etc. also increase readability if they are above the widget, or at least on the same indention level
On(
  password,
  shouldRebuild: (snapState){
    // some code
  }
  child: () =>
      TextField(
        onChanged: (String value) => password.state = Password(value),
        decoration: InputDecoration(
          hintText: "Password should be more than three characters",
          labelText: 'Password',
          errorText: password.error?.message,
        ),
    );
  },
)

The syntax would also improve features like On.future:

// instead of 
On.future<F>(
    onWaiting: ()=> Text('Waiting..'),
    onError: (error, refresher) => Text('Error'),//Future can be reinvoked
    onData: (data)=> MyWidget(),
).future(()=> anyKindOfFuture);

// this
On.future<F>(
    ()=> anyKindOfFuture
    onWaiting: ()=> Text('Waiting..'),
    onError: (error, refresher) => Text('Error'),//Future can be reinvoked
    onData: (data)=> MyWidget(),
)

Although On.Future would then probably not work with listenTo (but why should it anyways? One could use the regular On with an InjectFuture state?).

What do other States Rebuilder users/you think about the suggestion?

amoslai5128 commented 3 years ago

Yes, I agree the On. can be more flexible, how about?

  // In simple syntax
  password.onData((state) => Text(state));

  password.whenAll(onIdle: ..., onWaiting:)

  password.whenAny(...)

  // In widget-like syntax
  On(
    password,
    child: When.all(
      onIdle: null, // It'd be onData, then onWaiting if no data
      onError: (err, refresh) => Text('$err'),
      onData: () => Text('On Data'),
      onWaiting: () => const CircularProgressIndicator(),
    ),
  );

  OnCombined(
    [oldPassword, newPassword],
    child: When.any(
      onData: () => Text('On Data'),
      onWaiting: () => const CircularProgressIndicator(),
    ),
  );

  On.future(()=> future(), child: When.onData(...) )
samuelstroschein commented 3 years ago

@amoslai5128 I assume you want to move the rebuild logic to the child parameter because an On widget can return multiple children?

  // In widget-like syntax
  On(
    password,
    child: When.all(
      // child 1
      onIdle: null, // It'd be onData, then onWaiting if no data
      // child 2
      onError: (err, refresh) => Text('$err'),
      // child 3
      onData: () => Text('On Data'),
      // child 4
      onWaiting: () => const CircularProgressIndicator(),
    ),
  );

The more I look at the syntax the more I like it. However, the when syntax only shines for the all use case right? Because for example When.data only takes one child.

And I do like the On.all / On.future syntax. Probably child is only good for On widgets that have one child widgets and all other On widgets keep the current syntax like so:


// generic On widget without handling context
  On(
     password,
     child: () => Text('On Data')
)

// generic On widget with handling context
// using it as an example because developers new to states rebuilder will 
// replicate the FutureBuilde/ StreamBuilder sytax.
  On(
     password,
     // probably pass the state as parameter in child? 
     child: () { 
        if (password.isWaiting){
           return CircularProgressIndicator();
        } else if (password.hasError){
           return Text('something went wrong');
        }
        return Text('On Data')
     }
)

   // all widget
   On.all(
     password,
//  onIdle: what is the use-case for idle in a widget tree?
     onError: (err, refresh) => Text('$err'),
     onWaiting: () => const CircularProgressIndicator(), 
     onData: Text('On Data')
)

And since we are at it, why is onIdle beneficial in a widget tree? Wouldn't the developer always like to show the onData widget in that case?

amoslai5128 commented 3 years ago

The more I look at the syntax the more I like it. However, the when syntax only shines for the all use case right? Because for example When.data only takes one child. @samuelstroschein

Yes, the syntax looks lovely from your suggestions, making the child generic makes sense.

On the other hand, I think most of the time, the default side effect could be defined into RM.Inject, so in Widget side should often use for listening to the state onData changes only. Therefore, using On(foo, child: ()=>Text(foo.state.toString()));, the On looks a little bit confusing when I got a common side effect object of On.all passing it into multiple RM.injected, it may have a better naming?

How about On(foo, child: ()=> Text(...)).whenAny(waiting: ()=> const LinearProgressIndicator()); ?

final commonOnSideEffect = On.any(
    onIdle: () => const SizedBox(),
    onWaiting: () => const CircularProgressIndicator(),
    onError: (error) => showErrorDialog(error),
  );

final Injected<Foo> foo = RM.inject<Foo>(
  ()=> Foo(),
  onInitialized : (Foo state) => print('Initialized'),
  // Default callbacks for side effects.
  onSetState: commonOnSideEffect,
);

class HomePage extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
     // In your suggestion, If we only listen to onData
     return On(foo, child: ()=> Text(foo.state.toString()));
     // Not ideal if we want to override side effect , does it override the onWaiting at the parent?
     return On(foo, child: ()=> foo.isWaiting ? const LinearProgressIndicator() : Text(foo.state.toString()));

     // Better idea 1
     return On(foo, child: ()=> Text(...)).whenAny(waiting: ()=> const LinearProgressIndicator(), error: ..., idle: ... );

     // Better idea 2
     return When.any(foo, waiting: ()=>  const LinearProgressIndicator(), child: ()=> Text(...));
     return WhenCombined.any([foo1, foo2], waiting: ()=>  const LinearProgressIndicator(), child: ()=> Text(...));  
     // Sugar
     return foo.whenAny(waiting: ()=>  const LinearProgressIndicator(), child: ()=> .... );
     return [foo1, foo2].whenAny(waiting: ()=>  const LinearProgressIndicator(), child: ()=> .... );
  }
}

I think onIdle is somehow in a default condition when the state has not been initialized, if no onIdle & no onData it'd be showing onWaiting widget. What do you think? @GIfatahTH

GIfatahTH commented 3 years ago

@samuelstroschein

Another day, another suggestion.

Your welcome; your day makes my day.

The On widget moves rebuilding logic at the end of the widget, opposite to the previous function syntax. That becomes problematic the longer a widget tree becomes:

Yes. I was aware of this downside when building the On API.

All your proposals were on my table and I see very well the validity of your arguments. I like them all but I don't like breaking changes.

// In simple syntax password.onData((state) => Text(state)); password.whenAll(onIdle: ..., onWaiting:) password.whenAny(...)

I see that the idea of @amoslai5128 is a good point to start with.

samuelstroschein commented 3 years ago

How about On(foo, child: ()=> Text(...)).whenAny(waiting: ()=> const LinearProgressIndicator()); ?

Fabulous but the syntax moves a lot of valuable information which is not related to the Text widget at the end of the widget tree again. Something that prevents me from using the On widget.

How about:

On(
  foo, 
  shouldRebuild: (snapState) {
     // rebuild logic
  },
  when: When.any(
    waiting: () => const LinearProgressIndicator(),
  ),
  child: () => Text('...'),
)
On(
  foo, 
  when: When.waiting(
     () => const LinearProgressIndicator(),
  ),
  child: () => Text('...'),
)

Whereas the when parameter is optional. So one could write:

On(
  foo, 
  child: () => Text('...'),
)

Another benefit that this syntax is more "explorable" compared to On().when() because the IDE makes clear that a when parameter can be passed. While one would have to know by reading the docs that On().when() is a possibility.

Offtopic Shouldn't shouldRebuild() yield the middleSnapState instead of the snapState? Reason being that the middleSnapState allows comparing the current to the next state state. Even if most devs don't make use of the middleSnapState, theMIddleSnapState contains the "snapState"/next state. Should we open an issue about that too?

amoslai5128 commented 3 years ago

How about On(foo, child: ()=> Text(...)).whenAny(waiting: ()=> const LinearProgressIndicator()); ?

Fabulous but the syntax moves a lot of valuable information which is not related to the Text widget at the end of the widget tree again. Something that prevents me from using the On widget.

How about:

On(
  foo, 
  shouldRebuild: (snapState) {
     // rebuild logic
  },
  when: When.any(
    waiting: () => const LinearProgressIndicator(),
  ),
  child: () => Text('...'),
)
On(
  foo, 
  when: When.waiting(
     () => const LinearProgressIndicator(),
  ),
  child: () => Text('...'),
)

Whereas the when parameter is optional. So one could write:

On(
  foo, 
  child: () => Text('...'),
)

Another benefit that this syntax is more "explorable" compared to On().when() because the IDE makes clear that a when parameter can be passed. While one would have to know by reading the docs that On().when() is a possibility.

Offtopic Shouldn't shouldRebuild() yield the middleSnapState instead of the snapState? Reason being that the middleSnapState allows comparing the current to the next state state. Even if most devs don't make use of the middleSnapState, theMIddleSnapState contains the "snapState"/next state. Should we open an issue about that too?

Beautiful, this is the syntax that I'm looking for. The When.any / When.all just like a side effect module can be re-used for different injected states, making the code cleaner and easier maintenance.

Base On a state, When any situation (covering by side effect), finally, child represents the body of expectation.

On vs BaseOn what do you think?

samuelstroschein commented 3 years ago

I like On more because it's shorter. But besides, BaseOn should be BasedOn then it reads like "based on state do X" while "base on state" sounds like something is build on top of the state but not reacting on it.

GIfatahTH commented 3 years ago

On( foo, when: When.any( () => const LinearProgressIndicator(), ), child: () => Text('...'), )

What is the role of When here, is it for side effects or for widget rebuild? If it is for widget rebuild, the code above is not clear on what will be rendered?

Please, zoom out for the big picture to see all use cases and their positive and negative sides.

The reason why I like the On api, for me as a library maintainer, is its flexibility for adding dedicated rebuild widget for the custom Injected state: On.waiting, On.all, On.any, On.auth, On.crud, On.animation, On.form, On.tab (will be added soon for TabController),

If the only downside is moving the state at the end of the widget tree, I think that extension as proposed by @amoslai5128 is the way to go with.

samuelstroschein commented 3 years ago

On( foo, when: When.any( () => const LinearProgressIndicator(), ), child: () => Text('...'), )

What is the role of When here, is it for side effects or for widget rebuild? If it is for widget rebuild, the code above is not clear on what will be rendered?

True, When.any is confusing/should not exist. It's for widget rebuilds. So When.waiting, When.error would have been more appropriate.

The reason why I like the On api, for me as a library maintainer, is its flexibility for adding dedicated rebuild widget for the custom Injected state: On.waiting, On.all, On.any, On.auth, On.crud, On.animation, On.form, On.tab (will be added soon for TabController),

I find On better too. On.future(() -> myFuture) reads intuitively. Probably On could be On.state(() => injectedState).

If the only downside is moving the state at the end of the widget tree, I think that extension as proposed by @amoslai5128 is the way to go with.

Sure, that would improve the readability and would be good enough. I just observed that

However, the when syntax only shines for the all use case right? Because for example When.data only takes one child.

I was thinking a bit ahead here because in my mind When.data would have been a thing like so:

  // That seemed redundant.
  On(
    fooState,
    child: When.data(
      () => Text('On Data'),
    ),
  );

But of course, why would When.data exist? It should be When.any(data: () => child) and comparing @amoslai5128 proposal to mine e.g. On(state, child: When()) definitely reads better than On(state, when: When(), child: Widget). The latter one is indeed misleading what the actual child is.

Offtopic

I don't want to spam issues in this repo but what are your thoughts on:

Shouldn't shouldRebuild() yield the middleSnapState instead of the snapState? Reason being that the middleSnapState allows comparing the current to the next state state. Even if most devs don't make use of the middleSnapState, theMIddleSnapState contains the "snapState"/next state. Should I open another issue about that?

GIfatahTH commented 3 years ago

@ amoslai5128 @samuelstroschein from your fruitful discussion, I'm sure we will end with the best solution.

Offtopic

I don't want to spam issues in this repo but what are your thoughts on:

Shouldn't shouldRebuild() yield the middleSnapState instead of the snapState? Reason being that the middleSnapState allows comparing the current to the next state state. Even if most devs don't make use of the middleSnapState, theMIddleSnapState contains the "snapState"/next state. Should I open another issue about that?

I will check the feasibility.

amoslai5128 commented 3 years ago

@amoslai5128 @samuelstroschein from your fruitful discussion, I'm sure we will end with the best solution.

That's great!

amoslai5128 commented 3 years ago

All your proposals were on my table and I see very well the validity of your arguments. I like them all but I don't like breaking changes.

I agree with you that the library should try to be as consistent as possible, a smooth learning curve can persuade people to create tutorials on youtube or medium.

GIfatahTH commented 3 years ago

I come back after a period of vacation followed by another period of illness.

What do you think about this:

Alongside with:

    On(
      () => Text('Data'),
    ).listenTo(
      fooState,
      initState: () {
        print('initState');
      },
    );

We can also use

    fooState.on(
      () => Text('Data'),
      initState: () {
        print('initState');
      },
    );

Instead of

    On.all(
      onIdle: () => Text('OnIdle'),
      onWaiting: () => Text('onWaiting'),
      onError: (e, refresh) => Text('onError'),
      onData: () => Text('OnData'),
    ).listenTo(
      fooState,
      debugPrintWhenRebuild: '',
    );
    fooState.onAll(
      onIdle: () => Text('OnIdle'),
      onWaiting: () => Text('onWaiting'),
      onError: (e, refresh) => Text('onError'),
      onData: () => Text('OnData'),
      debugPrintWhenRebuild: '',
    );

SimilaryOn.or, On.auth, On.curd, On.animation, On.form, ... will be fooState.OnOr, fooState.onAuth, fooState.onCrud, fooState.onAnimation, fooState.onForm ...

What do think? Do you have a better naming convention?

amoslai5128 commented 3 years ago

I come back after a period of vacation followed by another period of illness.

What do you think about this:

Alongside with:

    On(
      () => Text('Data'),
    ).listenTo(
      fooState,
      initState: () {
        print('initState');
      },
    );

We can also use

    fooState.on(
      () => Text('Data'),
      initState: () {
        print('initState');
      },
    );

Instead of

    On.all(
      onIdle: () => Text('OnIdle'),
      onWaiting: () => Text('onWaiting'),
      onError: (e, refresh) => Text('onError'),
      onData: () => Text('OnData'),
    ).listenTo(
      fooState,
      debugPrintWhenRebuild: '',
    );
    fooState.onAll(
      onIdle: () => Text('OnIdle'),
      onWaiting: () => Text('onWaiting'),
      onError: (e, refresh) => Text('onError'),
      onData: () => Text('OnData'),
      debugPrintWhenRebuild: '',
    );

SimilaryOn.or, On.auth, On.curd, On.animation, On.form, ... will be fooState.OnOr, fooState.onAuth, fooState.onCrud, fooState.onAnimation, fooState.onForm ...

What do think? Do you have a better naming convention?

Yes, offering two approaches look lovely. However, the On & state.on may look confusing to the newcomers to understand on is for rebuilding the side-effects or widget.

GIfatahTH commented 3 years ago

Yes, offering two approaches look lovely. However, the On & state.on may look confusing to the newcomers to understand on is for rebuilding the side-effects or widget.

That was my impression too. What you propose?

What about state.rebuild((){}), state.rebuild.onData, state.rebuild.onAll, state.rebuild.onAnimation, ...

amoslai5128 commented 3 years ago

Yes, offering two approaches look lovely. However, the On & state.on may look confusing to the newcomers to understand on is for rebuilding the side-effects or widget.

That was my impression too. What you propose?

What about state.rebuild((){}), state.rebuild.onData, state.rebuild.onAll, state.rebuild.onAnimation, ...

I had a very similar thought today, if there is a panacea for saving maintenance cost from now on it'd be worth a breaking change. My thought was:

Widget builder only:
   1.  Rebuilder(state: stateA, widget: ()=> ....);
       MultiRebuilder(states: [stateA, stateB], widget: ()=> ....);

   2.  state.rebuilds(widget: ()=> Text());
       [stateA, stateB].rebuild(widget: ()=> ..., buildWhen: () => .... );

Side-Effect listener only:
   1.  StateListener(state: stateA, onState: (BuildContext context, On on) => ...  )
       MultiStateListener(states: [...], onState: (BuildContext context, On on) => ...  )

   2.  state.listens()  /  [stateA, stateB].listens(onState: (context, On on) =>  )

Mixed (widget & side-effect in one):
   1. state.consumes(onState: On.Waiting(()=> showIndicator())  , builder:  (data) ... , buildWhen: ) 

The reason I put the On inside the class constructor, it can be more flexible during the development, also benefits for those come from flutter_bloc.

state.listens(
   // Action for side-effects like navigation, snackbar
   onState: (BuildContext context, On on) {
       return on.Error((e) => showSnackbar(e));
   }
);

UPDATE: It should be StateListener

@GIfatahTH @samuelstroschein The last shot of breaking change to win people back, what do you think?

samuelstroschein commented 3 years ago

What do you think about this:

Alongside with:

    On(
      () => Text('Data'),
    ).listenTo(
      fooState,
      initState: () {
        print('initState');
      },
    );

We can also use

    fooState.on(
      () => Text('Data'),
      initState: () {
        print('initState');
      },
    );

The "problem" with the On widget remains. In a longer widget tree, it's not clear what On is reacting to (if buried somewhere a the end of the widget tree between endless parentheses and potentially other .listenTo). I don't mind the current API in regards to naming or consistency.

Widget builder only:

  1.  Rebuilder(state: stateA, widget: ()=> ....);
      MultiRebuilder(states: [stateA, stateB], widget: ()=> ....);

  2.  state.rebuilds(widget: ()=> Text());
      [stateA, stateB].rebuild(widget: ()=> ..., buildWhen: () => .... );

Why would two widget builders be beneficial instead of just one?

The reason I put the On inside the class constructor, it can be more flexible during the development, also benefits for those > come from flutter_bloc.

That means that the widget parameter takes the On class?

Rebuilder(state: stateA, widget: ()=> On.data(s) => ...);

amoslai5128 commented 3 years ago

The reason I put the On inside the class constructor, it can be more flexible during the development, also benefits for those > come from flutter_bloc.

That means that the widget parameter takes the On class?

Rebuilder(state: stateA, widget: ()=> On.data(s) => ...);

How about this?

StateConsumer(state: stateA ....)

// Or extension style

state.consumes(
  // For doing some expensive side-effects like navigation, snackbar
  onState: (BuildContext context, On on) {
       return on.Error((e, refresh) => showSnackbar(e));
   }
  // For rebuilding widget only
  widget: (On on)=> on.All(
    waiting: ()=> IndictorWidget(),
    error: (error, refresh)=> ErrorWidget(),
    data: (data)=> ListView(...)
  ),
);
GIfatahTH commented 3 years ago

@amoslai5128

StateConsumer(state: stateA ....)

// Or extension style

state.consumes( // For doing some expensive side-effects like navigation, snackbar onState: (BuildContext context, On on) { return on.Error((e, refresh) => showSnackbar(e)); } // For rebuilding widget only widget: (On on)=> on.All( waiting: ()=> IndictorWidget(), error: (error, refresh)=> ErrorWidget(), data: (data)=> ListView(...) ), );

Without any breaking change, we can get the same thing using:

//Here it is cear that state triggers the rebuild process.
state.rebuild.onAll(
  // For doing some expensive side-effects like navigation, snackbar
  onSetState: () {
       return on.Error((e, refresh) => showSnackbar(e));
   }
  // For rebuilding widget only
    waiting: ()=> IndictorWidget(),
    error: (error, refresh)=> ErrorWidget(),
    data: (data)=> ListView(...)
);

@samuelstroschein

The "problem" with the On widget remains. In a longer widget tree, it's not clear what On is reacting to (if buried somewhere a the end of the widget tree between endless parentheses and potentially other .listenTo). I don't mind the current API in regards to naming or consistency.

I don't see that. The new proposed API is similar to the existing fooState.rebuilder method (Which will be depricated if the the new API is adapted).

Look at this:

password.rebuild(
 () {
     //No matter how the long the widget is, It's clear that fooState is trigiring the rebuild when it is notified.
 }
);

It works for all On named constructor:

fooState.rebuild((){}), fooState.rebuild.onData,fooState.rebuild.onAll,fooState.rebuild.onAnimation, ...

amoslai5128 commented 3 years ago

I think it's necessary to separate into side-effect and widgets. Some people may confuse state.rebuild.onAll( with onSetState.

samuelstroschein commented 3 years ago

@samuelstroschein

The "problem" with the On widget remains. In a longer widget tree, it's not clear what On is reacting to (if buried somewhere a the end of the widget tree between endless parentheses and potentially other .listenTo). I don't mind the current API in regards to naming or consistency.

I don't see that. The new proposed API is similar to the existing fooState.rebuilder method (Which will be depricated if the the new API is adapted).

Look at this:

password.rebuild(
 () {
     //No matter how the long the s
widget is, It's clear that fooState is trigiring the rebuild when it is notified.
 }
);

It works for all On named constructor:

fooState.rebuild((){}), fooState.rebuild.onData,fooState.rebuild.onAll,fooState.rebuild.onAnimation, ...

Sure, but that's not the On widget. That's a function in a widget tree that returns a widget. By no means is that problematic but using the On widget in a widget tree makes the syntax more "flutterish" (including the syntax highlighting). It's a minor argument really but I thought the On widget is going to replace functions in widget trees. That does not seem to be the case which raises the question of why states rebuilder should provide two different ways to achieve the same thing (even though the API is similar)?

GIfatahTH commented 3 years ago

After taking the time to think about this question, I am still hesitant to move on to breaking changes without plausible benefits.

What I can do now is set for each On.foo(...).listen(state) widget a state.rebuild.onFoo(...) method and let everyone choose to use whatever they prefer.

Here's what we have:

     //Widget-like
       On(
          () => Text('${counter.state}'),
        ).listenTo(counter),

     //Method-like
        counter.rebuild(
          () => Text('{counter.state}'),
        ),
        //
     //Widget-like
        On.data(
          () => Text('${counter.state}'),
        ).listenTo(counter),

     //Method-like
        counter.rebuild.onData(
          (data) => Text(data),
        ),
     //Widget-like
       On.all(
          onIdle: () => Text('onIdle'),
          onWaiting: () => Text('onWaiting'),
          onError: (err, errorRefresh) => Text('onError'),
          onData: () => Text('{counter.state}'),
        ).listenTo(counter),

     //Method-like    
        counter.rebuild.onAll(
          onIdle: () => Text('onIdle'),
          onWaiting: () => Text('onWaiting'),
          onError: (err, errorRefresh) => Text('onError'),
          onData: () => Text('{counter.state}'),
        ),
        //
     //Widget-like
        On.or(
          onWaiting: () => Text('onWaiting'),
          or: () => Text('{counter.state}'),
        ).listenTo(counter),

     //Method-like
        counter.rebuild.onOr(
          onWaiting: () => Text('onWaiting'),
          or: () => Text('{counter.state}'),
        ),
     //Widget-like
      On.auth(
          onUnsigned: () => Text('Sign in/ up page'),
          onSigned: () => Text('Home page'),
        ).listenTo(
          user,
          useRouteNavigation: true,
        ),

     //Method-like
        user.rebuild.onAuth(
          onUnsigned: () => Text('Sign in/ up page'),
          onSigned: () => Text('Home page'),
          useRouteNavigation: true,
        ),
     //Widget-like
     On.crud(
          onWaiting: () => Text('onWaiting'),
          onError: (err, refreshError) => Text('onError'),
          onResult: (result) => Text('onResult'),
        ).listenTo(products),

     //Method-like
        products.rebuild.onCRUD(
          onWaiting: () => Text('onWaiting'),
          onError: (err, refreshError) => Text('onError'),
          onResult: (result) => Text('onResult'),
        ),
     //Widget-like
      On.animation((animate) {
          return Container(
            width: animate.fromTween((_) => Tween(begin: 0, end: 100)),
          );
        }).listenTo(myAnimation),

     //Method-like
        myAnimation.rebuild.onAnimation((animate) {
          return Container(
            width: animate.fromTween((_) => Tween(begin: 0, end: 100)),
          );
        }),
     //Widget-like
     On.form(
          () => TextField(
            controller: userName.controller,
          ),
        ).listenTo(form),

     //Method-like
        form.rebuild.onForm(
          () => TextField(
            controller: userName.controller,
          ),
        ),
        //
     //Widget-like
        On.formSubmission(
          onSubmitting: () => Text('onSubmitting'),
          child: Text('child'),
        ).listenTo(form),

     //Method-like
        form.rebuild.onFormSubmission(
          onSubmitting: () => Text('onSubmitting'),
          child: Text('child'),
        ),
     //Widget-like
        On.scroll(
          (scroll) => Text('onScroll'),
        ).listenTo(scroller),

     //Method-like
        scroller.rebuild.onScroll(
          (scroll) => Text('onScroll'),
        ),
     //Widget-like
      Scaffold(
          body: On.tab(
            () {
              return TabBarView(
                controller: myTab.controller,
                children: screens,
              );
            },
          ).listenTo(myTab),
          bottomNavigationBar: On.tab(
            () => TabBar(
              controller: myTab.controller,
              tabs: tabs,
            ),
          ).listenTo(myTab),
        ),

     //Method-like
        Scaffold(
          body: myTab.rebuild.onTab(
            () {
              return TabBarView(
                controller: myTab.controller,
                children: screens,
              );
            },
          ),
          bottomNavigationBar: myTab.rebuild.onTab(
            () => TabBar(
              controller: myTab.controller,
              tabs: tabs,
            ),
          ),
        ),
samuelstroschein commented 3 years ago

If you don't see enough justification for a breaking change, don't do it. It's a minor syntax "issue" which I thought could be easily fixed by making listenTo the first parameter instead of a suffix. Since the adoption of a state management solutions boils down to "how easy to use is it/how clean is the API?" I thought proposing the change might be helpful.

The solution you are proposing is not solving the issue I raised.

GIfatahTH commented 3 years ago

@samuelstroschein @amoslai5128

What about this:

final myState = 0.inj();
//In the widget tree

     OnBuilder(
          observe: myState,
          child: On.all(
            onIdle: () => Text('onIdle'),
            onWaiting: () => Text('onWaiting'),
            onError: (err, refreshError) => Text('onError'),
            onData: () => Text(myState.state.toString()),
          ),
          sideEffect: SideEffect(
            initState: () => print('initState'),
            onSetState: On(() => print('onSetState')),
            onAfterBuild: On(() => print('onAfterBuild')),
            dispose: () => print('dispose'),
          ),
          shouldRebuild: (oldSnap, newSnap) {
            return true;
          },
          debugPrintWhenRebuild: '',
     ),

Update

If you want to observe many states, just use observeManyparameter:

    OnBuilder(
          observeMany: [myState1, myState2],
          child: On.all(
            onWaiting: () => Text('onWaiting'), // Will be invoked if at least one state is waiting
            onError: (err, refreshError) => Text('onError'), // Will be invoked if at least one state has error
            onData: () => Text(myState.state.toString()), // Will be invoked if all states have data.
          ),
          sideEffect: SideEffect(
            initState: () => print('initState'),
            onSetState: On(() => print('onSetState')),
            onAfterBuild: On(() => print('onAfterBuild')),
            dispose: () => print('dispose'),
          ),

           /// shouldRebuild will take the oldSnap before mutation and the newSanp after mutation in the parameter
          shouldRebuild: (oldSnap, newSnap) {
            return true;
          },
          debugPrintWhenRebuild: '',
     ),

Notice that shouldRebuild will take the oldSnapbefore mutation and the newSanpafter mutation in the parameter as requested by @samuelstroschein

GIfatahTH commented 3 years ago

I think OnBuilder and its friends OnFooBuilder(OnAnimationBuilder, OnAuthBuilder, OnFormBuilder, ...) satisfy what you have requested,Don't they?

       OnBuilder(
           listenTo: counter,
           builder: On(() => Text('${counter.state}')),
           sideEffect: SideEffect(
            initState: () => print('initState'),
            onSetState: On(() => print('onSetState')),
            onAfterBuild: On(() => print('onAfterBuild')),
            dispose: () => print('dispose'),
          ),
          shouldRebuild: (oldSnap, newSnap) {
            return true;
          },
          debugPrintWhenRebuild: 'counter',
        ),

       OnBuilder(
           listenTo: counter,
           builder: On.data(
                () => Text('${counter.state}'),
               ),
        ),
       OnBuilder(
           listenTo: counter,
           builder: On.all(
                onIdle: () => Text('onIdle'),
                onWaiting: () => Text('onWaiting'),
                onError: (err, errorRefresh) => Text('onError'),
                onData: () => Text('{counter.state}'),
            ),
        ),

        OnBuilder(
           listenTo: counter,
           builder: On.all(
                onWaiting: () => Text('onWaiting'),
                or: () => Text('{counter.state}'),
            ),
        ),
        OnAuthBuilder(
          listenTo: user,
          onUnsigned: () => Text('Sign in/ up page'),
          onSigned: () => Text('Home page'),
          useRouteNavigation: true,
        ),
        OnCRUDBuilder(
          listenTo: products,
          onWaiting: () => Text('onWaiting'),
          onError: (err, refreshError) => Text('onError'),
          onResult: (result) => Text('onResult'),
        ),
        OnAnimationBuilder(
            listenTo: myAnimation
            builder: (animate) {
                return Container(
                    width: animate.fromTween((_) => Tween(begin: 0, end: 100)),
                );
           }
        ),
        OnFormBuilder(
          listenTo: myForm
          builder:() => TextField(
             controller: userName.controller,
           ),
        ),
        //
        OnFormSubmissionBuilder(
          listenTo: myForm
          onSubmitting: () => Text('onSubmitting'),
          child: Text('child'),
        ),
        OnScrollBuilder(
          listenTo: scroller,
          builder: (scroll) => Text('onScroll'),
        ),
        Scaffold(
          body: OnTabBuilder(
              listenTo: myTab,
              builder : () {
              return TabBarView(
                controller: myTab.controller,
                children: screens,
              );
            },
          ),
          bottomNavigationBar: 
          OnTabBuilder(
              listenTo: myTab,
              builder :() => TabBar(
              controller: myTab.controller,
              tabs: tabs,
            ),
          ),
        ),
samuelstroschein commented 3 years ago

Yes, they do! Although the "builder" seems unnecessary. On, OnScroll, OnForm reads nicer. But I see that dropping builder would conflict with On.data etc. in the case of On and potentially mixed up with OnForm -> On.form.

GIfatahTH commented 3 years ago

the "builder" seems unnecessary. On, OnScroll, OnForm reads nicer. But I see that dropping builder would conflict with On.data etc. in the case of On and potentially mixed up with OnForm -> On.form.

Yes, I add the Builder suffix, to avoid conflicts with already existing classes. On the other hand, adding Builder makes the API more Flutterish.

amoslai5128 commented 3 years ago

the "builder" seems unnecessary. On, OnScroll, OnForm reads nicer. But I see that dropping builder would conflict with On.data etc. in the case of On and potentially mixed up with OnForm -> On.form.

Yes, I add the Builder suffix, to avoid conflicts with already existing classes. On the other hand, adding Builder makes the API more Flutterish.

Yes, it looks Flutterish and friendly to a flutter developer. BTW, I'm thinking about On. can be in another form.

              // The extension after OnState is following the type of RM.inject.
              final counterA = RM.inject<int?>(() => 0);
              final counterB = RM.inject<int?>(() => 0);
              final formState = RM.injectForm();
              final scrollState = RM.injectScrolling();
              ....

              // Widget Builder
              OnState([counterA, counterB])
                  .builder(
                    idle: () => DefaultWidget(), 
                    waiting: () => Indicator(),  
                    error: (error, refresh) => ErrorWidger(),
                    data: () => Text(counter.state),  // !! Only this is required
                  )
                  .settings(  // (Override) Side-effects or State setting, sync to other states... 
                    rebuildWhenAllOnData: false,   //  countA || counterB onData will trigger to rebuild.
                    initState: () => print('initState'),
                    onSetState: On(() => print('onSetState')),
                  ),

              // Widget Builder
              OnState(formState).formBuilder(() => TextField(counter.state)),

              // Scroll Builder
              OnState(scrollState).scrollBuilder(
                builder: (scroll) => ScrollBar(controller: ....),
              )

Auto formated in VSCode: Screenshot 2021-08-12 at 8 31 30 PM

Screenshot 2021-08-12 at 8 28 33 PM

So they won't make any breaking changes and are easy to wrap with other widgets what do you think? @GIfatahTH , @samuelstroschein

GIfatahTH commented 3 years ago

@amoslai5128 Did you try it? Or it is an imaginary scenario.

BTW, I successfully implemented OnObswidget that is similar to Observerin mobx and Obx in getx packages where subscription to injected state is done implicitly.

   final counter1 = RM.inject(() => 0);
   final counter2 = 0.inj();
   //Works with getter
   int get counter3 => counter1.state + counter2.state;
// In the widget tree
    OnObs(
        () {
          return Column(
            children: [
              Text('${counter1.state}'),
              Builder(
                builder: (_) {
                  // Even its deep inside the child widget tree, counter2 is
                  // subscribed to the OnObs. (Not possible with getX)
                  return Text('${counter2.state}');
                },
              ),
            // Captures the dependency of getters. When ever counter3 changes, the widget is rebuilt.
            Text('$counter3'),
            ],
          );
        },
      );

You can also use state status flags:

      final counter = RM.inject(() => 0);
//In the widget tree
      OnObs(
        () {
          if (counter.isWaiting) {
            return Text('isWaiting');
          }
          if (counter.hasError) {
            return Text('Error');
          }
          return Text('${counter.state}');
        },
      );

//Or you can use onAll or onOr methods:

   OnObs(
        () {
          return counter.onAll(
            onWaiting: () => Text('isWaiting'),
            onError: (err, refreshErr) => Text('Error'),
            onData: (data) => Text('$data'),
          );
        },
      );

  OnObs(
        () {
          return counter.onOr(
            onWaiting: () => Text('isWaiting'),
            or: (data) => Text('$data'),
          );
        },
      );

What do you think?

samuelstroschein commented 3 years ago

So they won't make any breaking changes and are easy to wrap with other widgets what do you think? @GIfatahTH , @samuelstroschein

Hmmm, while it does have an appeal, it's yet another way to use states (rebuilder).

BTW, I successfully implemented OnObs widget that is similar to Observer in mobx and Obx in getx packages where subscription to injected state is done implicitly.

This is great! Are there any downsides? I would be keen to wrap every widget with OnObs and drop On and state.rebuilder() entirely. A better name for OnObs would probably be Reactive .

GIfatahTH commented 3 years ago

@samuelstroschein Why not OnReactive orOnRx, because I want the API to be more intuitive so that when the user taps On, the IDE displays all the available widgets to the choice.

BTW, your issues always push me to think beyond the limit I set for myself.

This is great! Are there any downsides?

Until now I don't see any. Even the limitation of GetX to not use a HeavyWidget inside one Obx, I get rid of it.

amoslai5128 commented 3 years ago

@amoslai5128 Did you try it? Or it is an imaginary scenario.

BTW, I successfully implemented OnObswidget that is similar to Observerin mobx and Obx in getx packages where subscription to injected state is done implicitly.

   final counter1 = RM.inject(() => 0);
   final counter2 = 0.inj();
   //Works with getter
   int get counter3 => counter1.state + counter2.state;
// In the widget tree
    OnObs(
        () {
          return Column(
            children: [
              Text('${counter1.state}'),
              Builder(
                builder: (_) {
                  // Even its deep inside the child widget tree, counter2 is
                  // subscribed to the OnObs. (Not possible with getX)
                  return Text('${counter2.state}');
                },
              ),
            // Captures the dependency of getters. When ever counter3 changes, the widget is rebuilt.
            Text('$counter3'),
            ],
          );
        },
      );

You can also use state status flags:

      final counter = RM.inject(() => 0);
//In the widget tree
      OnObs(
        () {
          if (counter.isWaiting) {
            return Text('isWaiting');
          }
          if (counter.hasError) {
            return Text('Error');
          }
          return Text('${counter.state}');
        },
      );

//Or you can use onAll or onOr methods:

   OnObs(
        () {
          return counter.onAll(
            onWaiting: () => Text('isWaiting'),
            onError: (err, refreshErr) => Text('Error'),
            onData: (data) => Text('$data'),
          );
        },
      );

  OnObs(
        () {
          return counter.onOr(
            onWaiting: () => Text('isWaiting'),
            or: (data) => Text('$data'),
          );
        },
      );

What do you think?

The screenshots are just an imaginary scenario, I was wrapping the extension with it. OnReactive or OnRx looks cool.

GIfatahTH commented 3 years ago

@amoslai5128 @samuelstroschein OK, I choose OnReactive. You can try the new dev version by using states_rebuilder: ^4.4.0-dev-1 in pubspec file.

The screenshots are just an imaginary scenario, I was wrapping the extension with it.

I had the same idea, and I was very tricky to implement. Once I get the right why to implement it we will discuss it again.

Here's an example that you can start with. This example if conveted to getX would throw

final items = List.generate(20, (index) => index).inj();
final filter = true.inj(); // true mean the number is even,

List<int> get filteredItems {
  if (filter.state) {
    return items.state.where((item) => item % 2 == 0).toList();
  } else {
    return items.state.where((item) => item % 2 == 1).toList();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('title'),
      ),
      body: OnReactive(//Will capture all listener for the child widget tree
        () => Column(
          children: [
            Row(
              children: [
                ToggleFilterWidget(),
                AddItemsWidget(),
              ],
            ),
            Expanded(
              child: ListBuilderWidget(),
            ),
          ],
        ),
      ),
    );
  }
}

class ToggleFilterWidget extends StatelessWidget {
  const ToggleFilterWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: () => filter.toggle(),
      icon: Icon(
        filter.state ? Icons.toggle_off : Icons.toggle_on,
      ),
    );
  }
}

class AddItemsWidget extends StatelessWidget {
  const AddItemsWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return OutlinedButton(
      onPressed: () => items.state = List.generate(
        items.state.length + 5,
        (index) => index,
      ),
      child: Text('Add items'),
    );
  }
}

class ListBuilderWidget extends StatelessWidget {
  const ListBuilderWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: filteredItems.length,
      itemBuilder: (ctx, index) {
        return Text('Item : ${filteredItems[index]}');
      },
    );
  }
}
amoslai5128 commented 3 years ago
 body: OnReactive( //Will capture all listener for the child widget tree
       () => Column(

So, your example above means the OnReactive can define on the root of that feature page, and each of inj() will hook up the corresponding widget to rebuild?

GIfatahTH commented 3 years ago

So, your example above means the OnReactive can define on the root of that feature page, and each of inj() will hook up the corresponding widget to rebuild?

Yes, but be aware that all the widget tree under OnReactive will rebuild.

amoslai5128 commented 3 years ago

@GIfatahTH Thank you for giving the amazing upcoming updates. I want to know the or: from myModel.onOr meaning undefined status? If so, does it run close in the default: in Switch Case?

OnReactive(
    ()=>
       //Or use on.Or method:
       myModel.on(
            onWaiting: ()=> WaitingWidget(),
            onDefault: ()=> DataWidget(), 
       );
)
GIfatahTH commented 3 years ago

@amoslai5128

Thank you for giving the amazing upcoming updates. I want to know the or: from myModel.onOr meaning undefined status? If so, does it run close in the default: in Switch Case?

Yes, It means non-defined status. Similar to when and maybeWhen in other libraries.

//Example from freezed 
//All callbacks are required
model.when(
waiting : ....,
error: ....,
data: ...
);

//Not all callbacks are required
model.maybeWhen(
waiting: ...,
orElse: ....,//fallback callback
);

In our case:

//All callbacks are required
model.onAll(
onWaiting : ....,
onError: ....,
onData: ...
);

//Not all callbacks are required
model.onOr(
onWaiting: ...,
or: ....,//fallback callback
);

To be more clearer I can rename or to orElse.

amoslai5128 commented 3 years ago

@amoslai5128

Thank you for giving the amazing upcoming updates. I want to know the or: from myModel.onOr meaning undefined status? If so, does it run close in the default: in Switch Case?

Yes, It means non-defined status. Similar to when and maybeWhen in other libraries.

//Example from freezed 
//All callbacks are required
model.when(
waiting : ....,
error: ....,
data: ...
);

//Not all callbacks are required
model.maybeWhen(
waiting: ...,
orElse: ....,//fallback callback
);

In our case:

//All callbacks are required
model.onAll(
onWaiting : ....,
onError: ....,
onData: ...
);

//Not all callbacks are required
model.onOr(
onWaiting: ...,
or: ....,//fallback callback
);

To be more clearer I can rename or to orElse.

Do you think otherwise: is a good idea too?

GIfatahTH commented 3 years ago

@amoslai5128 @samuelstroschein I am ready to publish the next update which is heavily influenced by this issue.

I want to wrap up.

OnBuilder(
    listenTo: myState,
    //called whenever myState emits a notification
    builder: () => Text('${counter.state}'),
    sideEffect: SideEffect(
        initState: () => print('initState'),
        onSetState: On(() => print('onSetState')),
        onAfterBuild: On(() => print('onAfterBuild')),
        dispose: () => print('dispose'),
    ),
    shouldRebuild: (oldSnap, newSnap) {
      return true;
    },
    debugPrintWhenRebuild: 'myState',
),

//Rebuild only when the myState emits notification with isData equals to true
OnBuilder.data(
    listenTo: myState,
    builder: (data) => Text('$data'),
),

//Handle all possible state status
OnBuilder.all(
    listenTo: myState,
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: () => Text('{myState.state}'), 
),

//Handle all possible state status with orElse fallback for the undefined status.
OnBuilder.orElse(
    listenTo: myState,
    onWaiting: () => Text('onWaiting'),
    orElse: () => Text('{myState.state}'),
),

For each OnBuilder widget flavor there is method like equivalent:

//Widget-like
OnBuilder(
    listenTo: myState,
    builder: () => Text('${myState.state}'),
),

//Method-like
myState.rebuild(
    () => Text('{myState.state}'),
),
//
//Widget-like
OnBuilder.data(
    listenTo: myState,
    builder: (data) => Text('$data')),
),

//Method-like
myState.rebuild.onData(
    (data) => Text(data),
),

//Widget-like
OnBuilder.all(
    listenTo: myState,
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: (data) => Text('{myState.state}'),
)

//Method-like    
myState.rebuild.onAll(
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: (data) => Text('{myState.state}'),
),
//
//Widget-like
OnBuilder.orElse(
    listenTo: myState,
    onWaiting: () => Text('onWaiting'),
    or: (data) => Text('{myState.state}'),
),

//Method-like
myState.rebuild.onOrElse(
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('{myState.state}'),
),

//Widget-like
OnBuilder.orElse(
    listenTo: [myState1 , myState2],
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('$data'),
),

//Method-like
[myState1 , myState2].rebuild.onOrElse(
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('$data'),
),

I would be very grateful if you could help me improve the readme and wiki docs.

amoslai5128 commented 3 years ago

@amoslai5128 @samuelstroschein I am ready to publish the next update which is heavily influenced by this issue.

I want to wrap up.

  • I added the OnReactive widget, which implicitly detects its listeners.
OnReactive(
    (){
        //Widget to rebuild
    }, 
    sideEffects: SideEffects(
        initState: (){
            // Side effect to call when the widget is first inserted into the widget tree
        },
        dispose: (){
            // Side effect to call when the widget is removed from the widget tree
        },
        onSetState: (snapState){
            // Side effect to call when is notified to rebuild

            //if the OnReactive listens to many states, the exposed snapState is that of the state that emits the notification
        },
    )
    shouldRebuild: (oldSnap, newSnap){
        // return bool to whether rebuild the widget or not.

        //if the OnReactive listens to many states, the exposed snapState is that of the state that emits the notification
    },

    //Debug prints an informative message when the widget is rebuilding with the name of the state that has emitted the notification.
    debugPrintWhenRebuild: 'custom name',
    //Debug prints an informative message when a state is added to the list of subscriptions.
    debugPrintWhenObserverAdd: 'custom name,
);
  • I also added OnBuilder widget with its named constructor variants OnBuilder.data; OnBuilder.all and OnBuilder.orElse;
OnBuilder(
    listenTo: myState,
    //called whenever myState emits a notification
    builder: () => Text('${counter.state}'),
    sideEffect: SideEffect(
        initState: () => print('initState'),
        onSetState: On(() => print('onSetState')),
        onAfterBuild: On(() => print('onAfterBuild')),
        dispose: () => print('dispose'),
    ),
    shouldRebuild: (oldSnap, newSnap) {
      return true;
    },
    debugPrintWhenRebuild: 'myState',
),

//Rebuild only when the myState emits notification with isData equals to true
OnBuilder.data(
    listenTo: myState,
    builder: (data) => Text('$data'),
),

//Handle all possible state status
OnBuilder.all(
    listenTo: myState,
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: () => Text('{myState.state}'), 
),

//Handle all possible state status with orElse fallback for the undefined status.
OnBuilder.orElse(
    listenTo: myState,
    onWaiting: () => Text('onWaiting'),
    orElse: () => Text('{myState.state}'),
),

For each OnBuilder widget flavor there is method like equivalent:

//Widget-like
OnBuilder(
    listenTo: myState,
    builder: () => Text('${myState.state}'),
),

//Method-like
myState.rebuild(
    () => Text('{myState.state}'),
),
//
//Widget-like
OnBuilder.data(
    listenTo: myState,
    builder: (data) => Text('$data')),
),

//Method-like
myState.rebuild.onData(
    (data) => Text(data),
),

//Widget-like
OnBuilder.all(
    listenTo: myState,
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: (data) => Text('{myState.state}'),
)

//Method-like    
myState.rebuild.onAll(
    onIdle: () => Text('onIdle'),
    onWaiting: () => Text('onWaiting'),
    onError: (err, errorRefresh) => Text('onError'),
    onData: (data) => Text('{myState.state}'),
),
//
//Widget-like
OnBuilder.orElse(
    listenTo: myState,
    onWaiting: () => Text('onWaiting'),
    or: (data) => Text('{myState.state}'),
),

//Method-like
myState.rebuild.onOrElse(
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('{myState.state}'),
),

//Widget-like
OnBuilder.orElse(
    listenTo: [myState1 , myState2],
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('$data'),
),

//Method-like
[myState1 , myState2].rebuild.onOrElse(
    onWaiting: () => Text('onWaiting'),
    orElse: (data) => Text('$data'),
),

I would be very grateful if you could help me improve the readme and wiki docs.

Yes, that's my pleasure, and it's also a good idea to add some commented example code into the source code, so the developer can get hints on vscode.

@samuelstroschein It'd be great if we can do it together, your document structure looks well-organized. I believe this change can grow up the community, it's impressive. 💯

@GIfatahTH Any schedule to push this update?

samuelstroschein commented 3 years ago

I would be very grateful if you could help me improve the readme and wiki docs.

@samuelstroschein It'd be great if we can do it together, your document structure looks well-organized. I believe this change can grow up the community, it's impressive. 💯

Sad news, as you might know, I recently graduated. Besides my studies, I was running my IoT company which used Flutter for the app, and States Rebuilder as the state management solution. Over the past weeks, I had to decide if I either go all-in on the company or do a master's, explicitly excluding the option to do both as that is "Nichts halbes und nichts ganzes". Ultimately, I decided to do a master's. I can run a company later, a master's I can only start now (studying in the EU is free, some countries even pay students to study). The latter means I folding the business and seize the development of the flutter app. Seizing the development of the flutter app also means that, for now, I am not using states rebuilder any longer and will not invest time in this OSS project.

@GIfatahTH Thanks for your in-depth answers on issues I opened and overall for creating and maintaining this great package! @amoslai5128 Thank you for participating in our discussions!

GIfatahTH commented 3 years ago

@samuelstroschein Thank you for your contribution. Hope to hear from you sooner.