a simple yet powerful state management technique for Flutter

Overview

States_rebuilder

pub package actions workflow codecov

`states_rebuilder` is Flutter state management combined with a dependency injection solution and an integrated router to get the best state management experience and speed up your development.

Documentation


Contributing

There are multiple ways and places you can contribute to make this package nicer, if you are wondering how to start please post an issue and we're here to help facilitate that.

  • Asking question, leaving suggestion
  • Code commits and pull requests
  • Youtube promotion or Medium tutorial

About states_rebuilder

It is a feature-rich state management sulution:

Performance

  • Predictable and controllable
  • Support for immutable / mutable state
  • Strictly rebuild control

Code Clean

  • Separation of UI & business logic
  • Coding business logic in pure Dart.
  • Zero Boilerplate without code-generation

User Friendly

  • Elegant and lightweight syntax
  • SetState & Animation in StatelessWidget.
  • Built-in dependency injection system
  • Rich built-in features
  • Saving states to localDB - Theme, multi-langs - Navigation, show dialogs without BuildContext

Effective Production

  • CREATE, READ, UPDATE, and DELETE (CRUD) from rest-API or database
  • User authentication and authorization
  • App themes management
  • Internalization and localization

Maintainable

  • Easy to test, mock the dependencies
  • State tracker middleware
  • Built-in debugging print function
  • Capable for complex Apps
Comments
  • On widget: improve readability by making listenTo the first positional argument

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

    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.

    // 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: <Widget>[
            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: <Widget>[
            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?

    opened by samuelstroschein 42
  • Could you do a example about Firestore?

    Could you do a example about Firestore?

    GIfatahTH,

    Thanks for your great works and you're a lifesaver, I do love this states_rebuilder.

    Could you offer a new example about Firestore based on example 007?
    It's more than happy with a user profile upload function or more complex practice!

    Btw, where happened to example 009?

    Cheers,

    opened by amoslai5128 37
  • [New Feature] InjectedTextEditing and InjectedForm

    [New Feature] InjectedTextEditing and InjectedForm

    TextEditingController is yet another controller that has a dedicated Injected state that will make it easier for us:

    • Create and automatically dispose of a TextEditingController,
    • Associate the TextEditingController with a TextField or TextFormField,
    • Change the value of the text and cache it,
    • Easily work with a collection of TextFields (Form).
    • Manage FocusNote, to dynamically change the focus to the next field to edit.

    Creation of the InjectedTextEditing state

    Let's take the case of two TextFields: one for the email and the other for the password.

    final email =  RM.injectTextEditing():
    
    final password = RM.injectTextEditing(
      text: '',
      selection: TextSelection.collapsed(offset: -1),
      composing: TextRange.empty,
      validator: (String? value) {
        if (value!.length < 6) {
          return "Password must have at least 6 characters";
        }
        return null;
      },
      autoValidate: true,
      autoDispose: true,
    );
    
    • text,selection and composing used as in Flutter.
    • The validator callback returns an error string or null if the field is valid.
    • By default, text input is automatically validated while the user is typing. If set to false, the input text is only validated by calling the email.validate () method.
    • The InjectedTextEditing is automatically deleted when it is no longer used. If you want to keep the data and use it in another TextField, setautoDispose to false.

    Link the InjectedTextEditing with a TextField:

    //Basically, define only the controller
    //No need to define onChange nor onSave callbacks
    TextField(
        controller: email.controller,
    ),
    
    //Or for full features
    TextField(
        controller: email.controller,
        focusNode: email.focusNode, //It is auto disposed of.
        decoration:  InputDecoration(
            errorText: email.error, //To display the error message.
        ),
        onSubmitted: (_) {
            //Focus on the password TextField after submission
            password.focusNode.requestFocus();
        },
    ),
    

    Consuming the valid data after submission:

    _submit() {
      if (email.isValid && password.isValid) {
          print('Authenticate with ${email.text} and ${password.text}');
      }
    }
    
    • You can reset any TextField to its initial text using:
    email.reset();
    password.reset();
    

    Working with forms:

    If the application you are working on contains dozens of TextFields, it becomes tedious to process each field individually. Form helps us collect many TextFields and manage them as a unit.

    Injected the form state:

    final form = RM.injectForm(
      //optional parameter
      autovalidateMode: AutovalidateMode.disable,
    );
    
    • InjectedForm has one optional parameter. “autovalidateMode” is of type “AutovalidateMode”. As in Flutter, It can take one of three enumeration values: - AutovalidateMode.disable: The form is validated manually by callingform.validate() - AutovalidateMode.always: The form is always validated - AutovalidateMode.onUserInteraction: The form is not validated until the user has started typing.

    Link InjectedForm to InjectedTextEditing states

    In the user interface, we put the TextFields that we want to associate with the form inside theOn.form widget:

    On.form(
      () => Column(
        children: <Widget>[
            TextField(
                focusNode: email.focusNode,
                controller: email.controller,
                decoration: InputDecoration(
                  errorText: email.error,
                ),
                onSubmitted: (_) {
                  //request the password node
                  password.focusNode.requestFocus();
                },
            ),
            TextField(
                focusNode: password.focusNode,
                controller: password.controller,
                decoration: new InputDecoration(
                  errorText: password.error,
                ),
                onSubmitted: (_) {
                  //request the submit button node
                  form.submitFocusNode.requestFocus();
                },
            ),
            ElevatedButton(
                focusNode: form.submitFocusNode,
                onPressed: (){
                    if(form.validate()){
                        _submit();
                    }
                },
                child: Text('Submit'),
            )
        ],
      ),
    ).listenTo(form),
    
    • We only use TextField widgets, no need to useTextFormField
    • To validate all fields, use: form.validate()
    • To reset all fields to their initial values, use: form.reset()
    • To check that all fields are valid, use form.isValid
    • Each InjectedTextEditing has an associatedFocusNode.
    • The InjectedForm is associated with a FocusNote to be used in the submit button.
    • All TextEditingControllers and FocusNotes are automatically disposed of when they are no longer in use.
    enhancement Implemented 
    opened by GIfatahTH 33
  • V. 3.0.0+1

    V. 3.0.0+1

    • Add state persistence feature. #134

    • Add RM.navigate for simple navigation. #129 Now, we use:

    MaterialApp(
      navigatorKey : RM.navigate.navigatorKey,
      //
    )
    
    //to  navigate:
    * RM.navigator.to(Page1());
    * RM.navigator.toNamed('/page1');
    * RM.navigator.toReplacement(Page1());
    * RM.navigator.toReplacementNamed('/page1');
    * RM.navigator.toAndRemoveUntil(Page1(), '/page2');
    * RM.navigator.pushNamedAndRemoveUntil('/page1', '/page2');
    * RM.navigator.back();
    * RM.navigator.backUntil('/page2');
    * RM.navigator.backAndToNamed('/page2');
    //To show dialogs, menu and bottom sheets
    * RM.navigator.toDialog => showDialog
    * RM.navigator.toCupertinoDialog => showCupertinoDialog
    * RM.navigator.toBottomSheet => showModalBottomSheet
    * RM.navigator.toCupertinoModalPopup => showCupertinoModalPopup
    //To show Scaffold related snackBars, bottom sheets and drawers
    * RM.scaffoldShow.bottomSheet => Scaffold.of(context).showBottomSheet,
    * RM.scaffoldShow.snackBar => Scaffold.of(context).showSnackBar,
    * RM.scaffoldShow.openDrawer => Scaffold.of(context).openDrawer,
    * RM.scaffoldShow.openEndDrawer => Scaffold.of(context).openEndDrawer,
    
    • Deprecate RM.navigator, RM.Scaffold and RM.show.
    • Refactor internal logic to improve performance.
    opened by GIfatahTH 31
  • hi, cannot inject new instance...

    hi, cannot inject new instance...

    Exception: Injecting an already injected model You are injecting the [ApiMultimedia6c443f00-5420-11ea-cac3-d9bdb02134f5a30798c0-4e6c-11ea-e7a2-3190ade73832] model which is already injected. This is not allowed. If we are sure you want to inject it again, try injecting it with custom name.

    Hi, I am very excited about this development. But I do not understand how to insert a new instance if I get the error that it is already injected.

    I have a list of items, and I want a separate instance for each one ... can you guide me Thanks !!

    opened by gustavobrian 24
  • NoSuchMethodError: The getter 'reactiveSingleton' was called on null

    NoSuchMethodError: The getter 'reactiveSingleton' was called on null

    I have just converted to the latest release (1.8.0+1) and am now getting a lot of the above errors.

    Nothing has changed in my code other than the use of getAsReactive instead of getAsModel.

    Here is a trace of the error

    I/flutter ( 6949): Instance of 'StreamStatesRebuilder<AppConfig>'
    E/flutter ( 6949): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: NoSuchMethodError: The getter 'reactiveSingleton' was called on null.
    E/flutter ( 6949): Receiver: null
    E/flutter ( 6949): Tried calling: reactiveSingleton
    E/flutter ( 6949): #0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:53:5)
    E/flutter ( 6949): #1      ReactiveModel._rebuildStates 
    package:states_rebuilder/src/reactive_model.dart:179
    E/flutter ( 6949): #2      ReactiveModel.setState 
    package:states_rebuilder/src/reactive_model.dart:164
    E/flutter ( 6949): #3      HomeScreen._buildItem.<anonymous closure>.<anonymous closure> 
    package:rapidlog/screens/HomeScreen.dart:87
    E/flutter ( 6949): #4      _rootRunUnary  (dart:async/zone.dart:1134:38)
    

    Here is the code I am using in one of the functions:

                  final config = Injector.getAsReactive<AppConfig>();
                  config.setState( (state) => state.add(newLogger) );
    

    It is behaving like a timing issue. Sometimes when I run the debugger and put a breakpoint on the first line, and step through it, config returns the value. Other times, config is not defined, and explicitly attempting to get its value in the debug console, I get the following error:

    '': error: org-dartlang-debug:synthetic_debug_expression:1:1: Error: The getter 'config' isn't defined for the class 'LibraryListScreen'.
     - 'LibraryListScreen' is from 'package:rapidlog/screens/LibraryListScreen.dart' ('lib/screens/LibraryListScreen.dart').
    Try correcting the name to the name of an existing getter, or defining a getter or field named 'config'.
    

    I read through the change log and saw the introduction of getAsReactive, and made the appropriate changes, but is there something else that I need to change in my code with this new release? (I was previously using 1.6.1)

    opened by SGarno 24
  • [Proposal] allow states to be null because null object pattern redundant with null safety?

    [Proposal] allow states to be null because null object pattern redundant with null safety?

    Introduction

    My very first opened issue #153 was caused by setting a userState to null to express that no user is logged in which failed silently (also related to #183). @GIfatahTH made me aware that states rebuilder requires a null object pattern as by default states can not be set to null https://github.com/GIfatahTH/states_rebuilder/issues/153#issuecomment-750448963. I would like to start a discussion if forcing the use of null object patterns by disallowing states to be nullable is still adding benefit with the introduction of Dart's null safety.

    Problem

    States Rebuilder forces developers to avoid null states by using the null object pattern e.g. instead of a authState being null when no user is logged in, the state is of type NoUser (or similar) which is an extension of the User class.

    The following example illustrates an authState before the null safety update:

    class User {
      String id;
      String email;
      String language;
    
      User({
        @required this.id,
        @required this.email,
        this.language,
      });
    }
    
    class NoUser extends User {}
    

    1. Problem: Awareness

    Users need to be aware of the null object pattern (avoid null), otherwise states rebuilder does not work as expected #153, #159.

    2. Problem: Conditional Logic

    NoUser is an extension of User and thus the type is also User. The following expectations do not hold and can lead to unforeseen bugs:

    
    // no user is logged in when the app starts (simplifies the example)
    final Injected<User> authState = RM.inject(() => NoUser());
    
    void main() {
      test("1. Problem: NoUser is also a User", () {
          expect(authState.state is NoUser, isTrue); // output: true
          expect(authState.state is User, isFalse); //  output: Exception. NoUser is User == true.  
        });
    }
    

    If developers make use of the null object pattern, they shall not make conditional logic based on the User class like if (variable is User) {} but only on the null object e.g. if (variables is NoUser){}.

    3. Problem: Null object pattern does not "play nice" with null safety

    The following code snippet adjusts the User and NoUser class as shown in the first example to null safety:

    class User {
      String id;
      String email;
      String? language;
    
      User({
        required this.id,
        required this.email,
        this.language,
      });
    }
    
    class NoUser extends User {
      NoUser() : super(id: "no user", email: "[email protected]");
    }
    

    In order to create an extension of the User class, the NoUser class now needs to initialize the non-nullable fields id and email resulting in a very counterintuitive assignation of a mock id and mock email. In other words, the null object pattern now forces giving a non-logged-in/non-existent user a user id.

    Proposed Solution

    Dart null-safety solves the problem of null values better than the null object pattern. Therefore, allow states to be nullable. In other words, a state defined as the following should be possible (currently it throws an error):

    // no user is logged in when the app starts (simplifies the example)
    final Injected<User?> authState = RM.inject(() => null);
    

    Injected<User?> makes it very clear that the state could hold no user aka no user is logged in and is good for the following reasons:

    1. Solves the awareness problem by being consistent with Dart syntax.
    2. Solves the conditional logic problem. Dart will force developers to ensure that the user is logged in as the userState.state can be null by either making a null check through if (userState.state != null) or userState.state! if the developer is sure that the user is logged in.
    3. Less verbose than null object pattern.

    Concluding Words

    States Rebuilder should allow states which are explicitly stated to be nullable e.g. Injected<User?> to be nullable. But besides that, am I missing the point of the null object pattern, or is it redundant and should be removed from the examples/force developers to use it?

    opened by samuelstroschein 22
  • call rebuildStates without any listeners

    call rebuildStates without any listeners

    I've just started using your great plugin, but quickly I had this error when calling rebuildStates : "No listener is registered yet"

    And yes, it's true, I have no listener set yet, there are added later in my app. Still I don't understand the error. For me it's like a ChangeNotifier or a Stream, you can call notifyListener() or add() even if there are nobody listening.

    opened by Nico04 19
  • How to avoid 'FlutterError (setState() or markNeedsBuild() called during build.'

    How to avoid 'FlutterError (setState() or markNeedsBuild() called during build.'

    Using Injected Model I am trying to fetch articles from end point. I am using TabBar to switch between different category.

      body: TabBarView(
              children: [
                ArticleListView(),
                ArticleListView(),
              ],
            ),
    

    Inside ArticleListView() I am using wpPageServiceIM.whenRebuilderOr(). While switching from tabbar (tabbar with same page) it gives exception saying 'FlutterError (setState() or markNeedsBuild() called during build.' How should I overcome this? I have single service to fetch article from end point final wpPageServiceIM = RM.inject<WpPageService>(() => WpPageService(api: _api.state)); While suing StateBuilder() solves the issue.

    I am new at programming, if my question is silly sorry for that.

    opened by 2shrestha22 15
  • [New widget] `TopStatelessWidget` on top of `MaterialApp` Widget for plugins initialization, app  internationalization and theme managmeent

    [New widget] `TopStatelessWidget` on top of `MaterialApp` Widget for plugins initialization, app internationalization and theme managmeent

    In previous releases we have TopAppWidget useful for:

    • Initialize plugins; display splashScreen while initialization, and error screen in case of failure.
    • Subscribe to InjectedI18N and InjectedTheme to internationalize the app and manage its theming.
    • Invoke side effects when app life cycle changes.

    TopAppWidget may look cumbersome and InjectedI18N and InjectedTheme must be explicitly specified.

    TopStatelessWidget is a rewrite of TopAppWidget to make it less boilerplate.

    In all cases just use TopStatelessWidget instead of StatelessWidget on top of MaterialApp widget.

    Plugins initialization

    In Flutter it is common to initialize plugins inside the main method:

    void main()async{
     WidgetsFlutterBinding.ensureInitialized();
     await initializeFirstPlugin();
     await initializeSecondPlugin();
     runApp(MyApp());
    }
    

    If you want to initialize plugins and display splash screen while waiting for them to initialize and display an error screen if any of them fails to initialize or request for permission with the ability to retry the initialization you can use TopStatelessWidget:

    class MyApp extends TopStatelessWidget {
      const MyApp({Key? key}) : super(key: key);
      @override
      List<Future<void>>? ensureInitialization() {
        return [
          initializeFirstPlugin(),
          initializeSecondPlugin(),
        ];
      }
      @override
      Widget? splashScreen() {
        return Material(
          child: Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          ),
        );
      }
      @override
      Widget? errorScreen(error, void Function() refresh) {
        return ElevatedButton.icon(
          onPressed: () => refresh(),
          icon: Icon(Icons.refresh),
          label: Text('Retry again'),
        );
      }
      @override
      Widget build(BuildContext context) {
        return MyHomePage();
      }
    }
    

    App internationalization and theme swishing

     void main() {
       runApp(MyApp());
     }
     final themeRM = RM.injectTheme(
       ...
     );
    
     final i18nRM = RM.injectedI18N(
       ...
     );
    
     class MyApp extends TopStatelessWidget {
       // This widget is the root of your application.
       @override
       Widget build(BuildContext context) {
         return MaterialApp(
           //
           theme: themeRM.lightTheme, //light theme
           darkTheme: themeRM.darkTheme, //dark theme
           themeMode: themeRM.themeMode, //theme mode
           //
           locale: i18nRM.locale,
           localeResolutionCallback: i18nRM.localeResolutionCallback,
           localizationsDelegates: [
             GlobalMaterialLocalizations.delegate,
             GlobalWidgetsLocalizations.delegate,
             GlobalCupertinoLocalizations.delegate,
           ],
           home: HomePage(),
         );
       }
     }
    

    App lifecycle

    To invoke side effects depending on the app life cycle, use didChangeAppLifecycleState hook

    class MyApp extends TopStatelessWidget {
      @override
      void didChangeAppLifecycleState(AppLifecycleState state) {
        print(state);
      }
      const MyApp({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    
    enhancement Implemented 
    opened by GIfatahTH 14
  • [New feature] InjectedAnimation

    [New feature] InjectedAnimation

    Controllers in Flutter can be seen as a class that is responsible for change a certain value and notify listeners. From this perspective, they are very related to state management.

    In the next update (4.2.0), I plan to add three controllers: InjectedAnimation, InjectedScroll, and InjectedTextEditing to abstract the use of AnimationController, ScrollController, and TextEditingController.

    Let's start with the InjectedAnimation:

    With InjectedAnimation, you can set animation implicitly without any limitation or explicitly with practically no boilerplate.

    Injecting animation:

    First we need to inject the animation:

    final animation = RM.injectAnimation(
      //Required parameter
      duration: const Duration(seconds: 2),
      //Optional parameters
      curve: Curves.linear,
      repeats: 2,
      shouldReverseRepeats: true,
      endAnimationListener: (){
        print('animation ends');
      }
    );
    
    • You have to define the duration of the animation.
    • The default curve is Curves.linear.
    • If you want the animation to repeat a certain number of times, you define the “repeats” argument.
    • If the animation is set to repeat, once the forward path is complete, it will go back to the beginning and start over.
    • If shouldReverseRepeats is set to true, the animation will repeat the cycle from start to end and reverse from end to start and so on.
    • endAnimationListener is used to performed side effects once the animation is finished.

    Animation is auto disposed of once no longer used. So do not worry about disposing of it.

    Implicit animation:

    Let's reproduce the AnimatedContainer example in official Flutter docs. (link here).

    In Flutter AnimatedContainer example, we see:

    Center(
        child: AnimatedContainer(
            width: selected ? 200.0 : 100.0,
            height: selected ? 100.0 : 200.0,
            color: selected ? Colors.red : Colors.blue,
            alignment: selected ? Alignment.center : AlignmentDirectional.topCenter,
            duration: const Duration(seconds: 2),
            curve: Curves.fastOutSlowIn,
            child: const FlutterLogo(size: 75),
        ),
    ),
    

    With states_rebuilder animation, we simply use the Container widget :

    Center(
        child: On.animation(
            (animate) => Container(
                // Animate is a callable class
                width: animate.call(selected ? 200.0 : 100.0),
                height: animate(selected ? 100.0 : 200.0, 'height'),
                color: animate(selected ? Colors.red : Colors.blue),
                alignment: animate(selected ? Alignment.center : AlignmentDirectional.topCenter),
                child: const FlutterLogo(size: 75),
            ),
        ).listenTo(animation),
    ),
    
    • On.animation or On.animation is used to listen to the injected animation.
    • On.animation exposes the animate function.
    • Using the exposed animate function, we set the animation start and end values.
    • As the width and height are the same types (double), we need to add a name to distinguish them.

    That's all, you are not limited to use a widget that starts with an Animated prefix to use implicit animation.

    Explicit animation

    In explicit animation, you have full control over how to parametrize your animation using tweens.

    On.animation(
        (animate) => Transform.rotate(
        angle: animate.formTween(
            (currentValue) => Tween(begin: 0, end: 2 * 3.14),
        )!,
        child: const FlutterLogo(size: 75),
        ),
    ).listenTo(animation),
    
    • The FlutterLogo will rotate from 0 to 2 * 3.14 (one turn)
    • The fromTween exposes the current value of the angle. It may be used to animate from the current value to the next value. (See the example below)

    For rebuild performance use Child, Child2 and Child3 widget.

    Example of a Clock:

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:states_rebuilder/states_rebuilder.dart';
    
    final animation = RM.injectAnimation(
        duration: const Duration(seconds: 1),
        curve: Curves.easeInOut,
        onInitialized: (animation) {
          Timer.periodic(
            Duration(seconds: 1),
            (_) {
              //rebuild the On.animation listeners, and recalculate the new implicit 
              //animation values
              animation.refresh();
            },
          );
        });
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Clock')),
            body: MyStatefulWidget(),
          ),
        );
      }
    }
    
    class MyStatefulWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Container(
            width: 200,
            height: 200,
            decoration: BoxDecoration(border: Border.all(width: 2.0)),
            child: Align(
              alignment: Alignment.topCenter,
              child: Child3(
                //Second rod
                child1: Container(
                  width: 1,
                  height: 100,
                  color: Colors.red,
                ),
                //minute rod
                child2: Container(
                  width: 2,
                  height: 90,
                  color: Colors.black,
                ),
                //hour rod
                child3: Container(
                  width: 4,
                  height: 80,
                  color: Colors.black,
                ),
                builder: (secondRod, minuteRod, hourRod) => Stack(
                  alignment: Alignment.bottomCenter,
                  children: [
                    On.animation(
                      (animate) => Transform.rotate(
                        angle: animate.formTween(
                          (currentValue) => Tween(
                            begin: currentValue ?? 0,
                            end: (currentValue ?? 0) + 2 * 3.14 / 60,
                          ),
                        )!,
                        alignment: Alignment.bottomCenter,
                        child: secondRod,
                      ),
                    ).listenTo(animation),
                    On.animation(
                      (animate) => Transform.rotate(
                        angle: animate.formTween(
                          (currentValue) => Tween(
                            begin: currentValue ?? 0,
                            end: (currentValue ?? 0) + 2 * 3.14 / 60 / 60,
                          ),
                        )!,
                        alignment: Alignment.bottomCenter,
                        child: minuteRod,
                      ),
                    ).listenTo(animation),
                    On.animation(
                      (animate) => Transform.rotate(
                        angle: animate.formTween(
                          (currentValue) => Tween(
                            begin: currentValue ?? 0,
                            end: (currentValue ?? 0) + 2 * 3.14 / 60 / 60 / 60,
                          ),
                        )!,
                        alignment: Alignment.bottomCenter,
                        child: hourRod,
                      ),
                    ).listenTo(animation),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    

    This is the output of this example:

    image

    Head to the wiki page to read more information about InjectedAnimation

    opened by GIfatahTH 14
  • Alternative to model.streamBuilder()?

    Alternative to model.streamBuilder()?

    In previous versions, you were able to use 'model.streamBuilder()' and have it listen to a value stream. Now with the most recent update, you can no longer do this. What is the alternative to this method?

    opened by am2074 1
  • Bottom navigation with named routing?

    Bottom navigation with named routing?

    Is it possible with states_rebuilder to have a bottom navigation bar with named routing? The bottom navigation bar would also need its own navigation stack per tab.

    opened by Reprevise 2
  • [Question] How to work with multiple files which need to access and update the same state?

    [Question] How to work with multiple files which need to access and update the same state?

    Like if I have a widget with structure

    Injector(
            inject: <Injectable>[Inject(() => MyState())],
            builder: (context) {
                ReactiveModel<MyState> myState =
                    Injector.getAsReactive<MyState>();
                    return SomeWidget(
                        child: ChildWidget(),
                    )
            }
    )
    

    Where MyState can be anything but let's assume

    class MyState {
      String? name;
      int? id;
    
      MyState({this.name, this.id});
    
      setName(String myName){
            name = myName;
        }
    
      setId(int myId){
            id = myId;
        }
    }
    

    Now I want to access / update myState from ChildWidget which is in a different dart file. I could not find help on this in docs. What should be the right way to go about it?

    opened by basic-bhavya 1
  • Learn with the Examples

    Learn with the Examples

    Here is the starting point of your journey with states_rebuilder.
    You'll learn from simple and incremental examples the core foundation of states_rebuilder. At the end of these sets of examples, you will get the correct answer why to use states_rebuilder.

    State management concepts

    Get the foundation of state management from very basic to more advanced concepts

    Navigation

    Development booster.

    Based on state management principles and some good programming principles and abstraction techniques, I created dedication injected state to automatize the most repetitive tasks a developer do.

    Question & Suggestion

    • Each example is a seed for a tutorial. It would be very encouraging if you wrote one.
    • Please feel free to post an issue or PR, as always, your feedback makes this library become better and better.
    good first issue 
    opened by GIfatahTH 6
  • Local state and the use of .inherited - where to initState/setStateof the local RM?

    Local state and the use of .inherited - where to initState/setStateof the local RM?

    Even after reading all resources covering local states I was still not able to figure out how to solve my current problem using v5.0.0.

    My app makes use of some (highly) reusable widgets (image avatars) that have its own "mini-store / view model" which takes care of downloading an image resource. As this widgets occur multiple times on a single page, they all need their own reactive model with its own state.

    I'm very used to the "old way" states rebuilder worked (using it for 2 years), but due to migration to null safety and so on it was time to upgrade the package.

    In the old days, I used RM.create() to obtain a reactive model that was not injected globally. So each widget had its very own instance of the "mini-store / view model" and the corresponding reactive model to handle the local state. In combination with WhenRebuilder my setup worked as expected. In the observe parameter, I created the new local RM instance, and within the initState method I told the RM to start downloading the image and handle state, see Code below ("OLD WAY").

    Now moving forward and using states rebuilder version ^5.0.0, I have some really hard time making this setup work by using .inherited as the docs tell me.

    First, I globally inject the mini-store (aka wikiImageStoreInjected) with throw UnimplementedError(), as mentioned in the docs. Second, I use the global variable with .inherited() in the widget tree, to create a local instance at that place in the widget tree. The two important params here are statesOverride and builder.

    As parameter for statesOverride, I return a fresh instance of WikiImageStore(...). Additionally, in the builder parameter, I create the avatar widget and try to access the downloaded image via wikiImageStoreInjected.of(context).imageUrl.

    Problem is: where would I tell the local RM / mini-store to download the image and call setState? a) In the statesOverride Parameter, I only have access to the local singleton but not not to the local RM which would allow me to call localRM.setState((s)=> s.getImage()). b) in the builder method, by using wikiImageStore.of(context), it's the same problem. a + b) I tried calling wikiImageStore.of(context).getImage() wihtout the use of an RM, and the image will be downloaded, but the widgets won't display because of missing setState statement. (Using hot reload forces state update and then I can see the images) c) in the builder method, instead of using .of() but using wikiImageStore(context), I can access a RM, but calling wikiImageStore(context).setState(...) gives me errors telling me that the widget is already being built and I cannot mark it as needToRebuild again.

    So to make it short: how to handle setState statements (e.g. for fetching data/use of futures) within .inherited structure?

    OLD WAY

    @override
      Widget build(BuildContext context) {
        return WhenRebuilder(
          observe: () => RM.create(WikiImageStore(
              wikiRepository: Injector.get<WikiRepository>())), //_storeRM,
          initState: (_, storeRM) => storeRM.setState((_) =>
              wikiUrl != null ? storeRM.state.getWikiImageUrl(wikiUrl) : null),
          onIdle: () => Container(),
          onWaiting: () => _buildImageContainer(_buildImagePlaceHolder()),
          onData: (store) => _buildImageContainer(store.imageUrl != null
              ? _buildImage(store.imageUrl)
              : _buildImagePlaceHolder()),
          onError: (_) => _buildImageContainer(_buildImagePlaceHolder()),
        );
      }
    

    NEW WAY:

    final Injected<WikiImageStore> wikiImageStoreInjected = RM.inject(
      () => throw UnimplementedError(),
    );
      @override
      Widget build(BuildContext context) {
        return wikiImageStoreInjected.inherited(
          stateOverride: () =>
              WikiImageStore(wikiRepository: wikiRepositoryInjected.state),
          // ..getWikiImageUrl(wikiUrl!),
          builder: (context) => _buildImageContainer(
            _buildImage(wikiImageStoreInjected.of(context).imageUrl!),
          ),
        );
      }
    

    Error message displayed when calling wikiImageStoreInjected(context).setState(): Exception has occurred. FlutterError (setState() or markNeedsBuild() called during build. This StateBuilderBase<_OnWidget<Widget>> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase. The widget on which setState() or markNeedsBuild() was called was: StateBuilderBase<_OnWidget<Widget>> The widget which was currently being built when the offending call was made was: OnBuilder<DriverDetailStore>)

    good first issue 
    opened by indigo-dev 13
Owner
MELLATI Fatah
MELLATI Fatah
Application for pomodoro technique in flutter

Application for pomodoro technique in flutter

Lubomír Žižka 3 Dec 29, 2022
A powerful state machine for MobX management, that can be used in almost any application state.

A powerful state machine for MobX management, which can be used in almost any application state. It has 3 states - loading, success, error - and is pe

Daniel Magri 8 Oct 31, 2022
A simple yet powerful Flutter plugin for showing Toast at Android, iOS and Web.

Flutter Toast A simple yet powerful Flutter plugin for showing Toast at Android and iOS. Features: Native Toast Pure Flutter Toaster Installation Add

Eyro Labs 5 Dec 13, 2021
⚡FQuery is a powerful async state management solution for flutter. It caches, updates and fully manages asynchronous data in your flutter apps.

⚡ FQuery is a powerful async state management solution for flutter. It caches, updates and fully manages asynchronous data in your flutter apps. It ca

Piyush 21 Dec 22, 2022
A light, powerful and reactive state management for Flutter Apps.

A light, powerful and reactive state management. Features ⚡️ Build for speed. ?? Reduce boilerplate code significantly. ?? Improve code readability. ?

2devs 3 Dec 15, 2022
State Persistence - Persist state across app launches. By default this library store state as a local JSON file called `data.json` in the applications data directory. Maintainer: @slightfoot

State Persistence Persist state across app launches. By default this library store state as a local JSON file called data.json in the applications dat

Flutter Community 70 Sep 28, 2022
Practice building basic animations in apps along with managing app state by BLoC State Management, Flutter Slider.

Practice building basic animations in apps along with managing app state by BLoC State Management including: Cubit & Animation Widget, Flutter Slider.

TAD 1 Jun 8, 2022
An extension to the bloc state management library which lets you create State Machine using a declarative API

An extension to the bloc state management library which lets you create State Machine using a declarative API

null 25 Nov 28, 2022
Shopify Tag and Product Management App using Flutter and Riverpod State Management

Myshopify App A Simple Flutter Application project to get List of Tags, Products and Product Details from shopify https://shopicruit.myshopify.com/adm

Idowu Tomiwa 5 Nov 12, 2022
A simple yet elegant tip calculator created using flutter framework and dart language.

CAL- TIP, A TIP CALCULATOR APPLICATION A simple yet elegant tip calculator created using flutter framework and dart language. As the name suggests, th

Nitin Verma 0 Dec 26, 2021
This project follows the Reso Coder course for flutter test-driven-development with clean architecture and BloC state management for a random trivia simple app.

This project follows the Reso Coder course for flutter test-driven-development with clean architecture and BloC state management for a random trivia simple app.

Tomas B Sarmiento Abella 1 Jan 5, 2022
Building a simple Flutter app for understanding the BLoC State Management including: Cubit, Managing Route & showSnackBar.

Building a simple Flutter app for understanding the BLoC State Management including: Cubit, Managing Route & showSnackBar.

TAD 8 Dec 3, 2022
Building a simple Flutter app * Switch Theme * for understanding the BLoC State Management including: Cubit Communications with StreamSubscription & Managing Route.

Building a simple Flutter app * Switch Theme * for understanding the BLoC State Management including: Cubit Communications with StreamSubscription & Managing Route.

TAD 1 Oct 3, 2022
Practice code of simple flutter app to explain how provider works as across the widget state management.

vdo A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is y

Syed Uzair 9 Nov 7, 2022
A simple to-do list built using flutter based on BLoC state management to manage your daily tasks .

?? Table of Contents About ScreenShots from the app Demo vedio Contributors About A to-do list flutter app to manage your daily tasks. it is built bas

Heba Ashraf 6 Oct 12, 2022
An atomic state management library for dart (and Flutter). Simple, fast and flexible.

nucleus An atomic dependency and state management toolkit. Design goals Simplicity - simple API with no surprises Performance - allow for millions of

Tim 12 Jan 2, 2023
A simple state management solution that combine the power of inherited widget and rxdart

Inherited RxDart is a state management library that combine the power of InheritedWidget and RxDart, a simple and elegant state management solution for apps of any scale

Nguyễn Ngọc Phước 14 Oct 26, 2022
Yet another Todo app, now using Flutter (with ScopedModel)

Flutter Todo Yet another Todo app, now using Flutter. Getting Started This Todo app is implemented using Flutter (with Scoped Model for state manageme

Tuan Nguyen 107 Jan 4, 2023
Yet another localization approach in Flutter

Flutter Global Summit Vol.2 schedule Source code of the mobile application that displays the schedule of the Flutter Global Summit Vol.2 conference th

Anna Leushchenko 3 Mar 24, 2022