GIfatahTH / states_rebuilder

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

[New feature] `OnReactive` widget; the best widget listener #209

Closed GIfatahTH closed 2 years ago

GIfatahTH commented 3 years ago

Referring to issue #205 , I get out with a new widget; The OnReactive

OnReactive widget is a new widget used to rebuild a part of the widget tree in response to state change.

OnReactive implicitly subscribes to injected ReactiveModels based on the getter ReactiveModel.state called during rebuild.

Example:

final counter1 = RM.inject(()=> 0) // Or just use extension: 0.inj()
final counter2 = 0.inj();
int get sum => counter1.state + counter2.state;

//In the widget tree:
Column(
    children: [
        OnReactive( // Will listen to counter1
            ()=> Text('${counter1.state}');
        ),
        OnReactive( // Will listen to counter2
            ()=> Text('${counter2.state}');
        ),
        OnReactive(// Will listen to both counter1 and counter2
            ()=> Text('$sum');
        )
    ]
)

Note that counter1 and counter2 are global final variable that holds the state. They are disposed automatically when not in use (have no listener).

You can scope the counter1 and counter2 variable and put them inside a class:

class CounterModel {
    final counter1 = RM.inject(()=> 0) // Or just use extension: 0.inj()
    final counter2 = 0.inj();
    int get sum => counter1.state + counter2.state;

    void increment1() => counter1.state++;
    void increment2() => counter2.state++;

    void asyncMethod() => counter1.setState((s) async => asyncRep())
}
//Just instantiate a global instance of the CounterModel and use it throughout your app. The CounterModel instance is not a global state rather it acts like a container that contains the counter1 and counter2 states.

//You can easily test the app and make sure the all states are reset to their initial state between tests.
final counterModel = CounterModel();

//In the widget tree:
Column(
    children: [
        OnReactive( // Will listen to counter1
            ()=> Text('${counterModel.counter1.state}');
        ),
        OnReactive( // Will listen to counter2
            ()=> Text('${counterModel.counter2.state}');
        ),
        OnReactive(// Will listen to both counter1 and counter2
            ()=> Text('${counterModel.sum}');
        )
    ]
)

OnReactive can listen to any state called form its child widget tree, no matter how deep the widget tree is.

OnReactive(
    ()=> DeepWidgetTree(),
)

class DeepWidgetTree extends StatelessWidget{
    Widget builder (BuildContext context){
        return Column(
            children: [
                //Will look up the widget tree and subscribe (if not already subscribed) to the first found OnReactive widget
                Text('${counter1.state}'),
                AnOtherChildWidget();
            ]
        );
    }
}

class DeepWidgetTree extends StatelessWidget {
    Widget builder (BuildContext context){
        //Will look up the widget tree and subscribe (if not already subscribed) to the first found OnReactive widget
        return Text('${counter2.state}');
    }
}

Inside OnReactiveyou can call any of the available state status flags (isWaiting, hasError, hasData, ...) or just use onAll and onOr methods:

OnReactive(
    ()=> {
        if(myModel.isWaiting){
            return WaitingWidget();
        }
        if(myModel.hasError){
            return ErrorWidget();
        }
        return DataWidget();
    }
)
//Or use onAll method:
OnReactive(
    ()=> {
        myModel.onAll(
            onWaiting: ()=> WaitingWidget(),
            onError: (err, refreshErr)=> ErrorWidget(),
            onDate: (data)=> DataWidget(),
        );
    }
)

//Or use onOr method:
OnReactive(
    ()=> {
        myModel.onAll(
            onWaiting: ()=> WaitingWidget(),
            or: (data)=> DataWidget(),
        );
    }
)

OnReactive is a lightweight widget that behaves as it is a const widget. That is, once OnReactive is created, it will not recreate after parent widgets rebuild. OnReactive is rebuild only after any observable state emits a notification or after hot restart.

This is the full API of OnReactive:

OnReactive(
    (){
        //Widget to rebuild
    }, 
    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
    },
);

Try is using the published dev version.

UPDATE

If you want a widget and all its child to rebuild in response to state mutation, just use ReactiveStatelessWidget instead of StatelessWidget for the parent widget.

class CounterModel {
    final counter1 = RM.inject(()=> 0) // Or just use extension: 0.inj()
    final counter2 = 0.inj();
    int get sum => counter1.state + counter2.state;

    void increment1() => counter1.state++;
    void increment2() => counter2.state++;

    void asyncMethod() => counter1.setState((s) async => asyncRep())
}
final counterModel = CounterModel();

///Use ReactiveStatelessWidget instead of StatelessWidget
class MyParentWidget extends ReactiveStatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _Widget1(),
        _Widget2(),
        _Widget3(),
      ],
    );
  }
}

class _Widget1 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.counter1.state}');
  }
}

class _Widget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.counter2.state}');
  }
}

class _Widget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.sum}');
  }
}
amoslai5128 commented 3 years ago

Amazing! Now it's easier to deal with the MVVM.

xalikoutis commented 3 years ago

Thats truly an MVVM approach which amazes me too

GIfatahTH commented 3 years ago

@xalikoutis @amoslai5128 Thanks for the comments. I want to know what you mean by truly an MVVM? I want to learn from you.

nixcode1 commented 3 years ago

I want to learn also, please do explain

xalikoutis commented 3 years ago

Before flutter i was a Xamarin Dev. Xamarin uses MVVM pattern in order to separate your business logic from the UI. It consists from

  1. a Model(Person)
  2. a ViewModel(ViewModelPerson) with all the business logic
  3. and the UI written in XML

you can bind your ViewModel properties and commands to the UI, like you also do in angular and vuejs, and notify the view of any state changes through change notification events. This binding is 2 way back and forth.

The ReactiveWidget works in a similar way as a handler for change notification events in the ViewModel(CounterModel). So @GIfatahTH i believe we are closer as we can get to MVVM pattern which i really love for its simplicity

amoslai5128 commented 3 years ago

The upcoming update, OnReactive(), looks so clean in the UI layer, this beautiful combination is the second time I've seen ever in Flutter, since Stacked with its architecture.

I think MVVM's kind is a lightweight version of Clean Architecture Design, and very friendly to small project, the scope is trying to separate the logic out between the UI and services, data source layer by setting up a ViewModel as a bridge. Therefore, the UI code would look good to get maintenance.

If somebody had experience in VueJs would probably know, and sadly I cannot find updated article for flutter, even though the widget style is very suitable for it.

GIfatahTH commented 3 years ago

Here is another improvement: If you want a widget and all its child to rebuild in response to state mutation, just use ReactiveStatelessWidget instead of StatelessWidget for the parent widget.

class CounterModel {
    final counter1 = RM.inject(()=> 0) // Or just use extension: 0.inj()
    final counter2 = 0.inj();
    int get sum => counter1.state + counter2.state;

    void increment1() => counter1.state++;
    void increment2() => counter2.state++;

    void asyncMethod() => counter1.setState((s) async => asyncRep())
}
final counterModel = CounterModel();

///Use ReactiveStatelessWidget instead of StatelessWidget
class MyParentWidget extends ReactiveStatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _Widget1(),
        _Widget2(),
        _Widget3(),
      ],
    );
  }
}

class _Widget1 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.counter1.state}');
  }
}

class _Widget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.counter2.state}');
  }
}

class _Widget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('${counterModel.sum}');
  }
}
xalikoutis commented 3 years ago

@GIfatahTH you nailed it

nixcode1 commented 3 years ago

@GIfatahTH Can't wait for the update!