A lightweight, yet powerful way to bind your application state with your business logic.

Overview

Build Pub Codecov

binder

Logo

A lightweight, yet powerful way to bind your application state with your business logic.

The vision

As other state management pattern, binder aims to separate the application state from the business logic that updates it:

Data flow

We can see the whole application state as the agglomeration of a multitude of tiny states. Each state being independent from each other. A view can be interested in some particular states and has to use a logic component to update them.

Getting started

Installation

In the pubspec.yaml of your flutter project, add the following dependency:

dependencies:
  binder: <latest_version>

In your library add the following import:

import 'package:binder/binder.dart';

Basic usage

Any state has to be declared through a StateRef with its initial value:

final counterRef = StateRef(0);

Note: A state should be immutable, so that the only way to update it, is through methods provided by this package.

Any logic component has to be declared through a LogicRef with a function that will be used to create it:

final counterViewLogicRef = LogicRef((scope) => CounterViewLogic(scope));

The scope argument can then be used by the logic to mutate the state and access other logic components.

Note: You can declare StateRef and LogicRef objects as public global variables if you want them to be accessible from other parts of your app.

If we want our CounterViewLogic to be able to increment our counter state, we might write something like this:

/// A business logic component can apply the [Logic] mixin to have access to
/// useful methods, such as `write` and `read`.
class CounterViewLogic with Logic {
  const CounterViewLogic(this.scope);

  /// This is the object which is able to interact with other components.
  @override
  final Scope scope;

  /// We can use the [write] method to mutate the state referenced by a
  /// [StateRef] and [read] to obtain its current state.
  void increment() => write(counterRef, read(counterRef) + 1);
}

In order to bind all of this together in a Flutter app, we have to use a dedicated widget called BinderScope. This widget is responsible for holding a part of the application state and for providing the logic components. You will typically create this widget above the MaterialApp widget:

BinderScope(
  child: MaterialApp(
    home: CounterView(),
  ),
);

In any widget under the BinderScope, you can call extension methods on BuildContext to bind the view to the application state and to the business logic components:

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

  @override
  Widget build(BuildContext context) {
    /// We call the [watch] extension method on a [StateRef] to rebuild the
    /// widget when the underlaying state changes.
    final counter = context.watch(counterRef);

    return Scaffold(
      appBar: AppBar(title: const Text('Binder example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text('$counter', style: Theme.of(context).textTheme.headline4),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        /// We call the [use] extension method to get a business logic component
        /// and call the appropriate method.
        onPressed: () => context.use(counterViewLogicRef).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

This is all you need to know for a basic usage.

Note: The whole code for the above snippets is available in the example file.


Intermediate usage

Select

A state can be of a simple type as an int or a String but it can also be more complex, such as the following:

class User {
  const User(this.firstName, this.lastName, this.score);

  final String firstName;
  final String lastName;
  final int score;
}

Some views of an application are only interested in some parts of the global state. In these cases, it can be more efficient to select only the part of the state that is useful for these views.

For example, if we have an app bar title which is only responsible for displaying the full name of a User, and we don't want it to rebuild every time the score changes, we will use the select method of the StateRef to watch only a sub part of the state:

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

  @override
  Widget build(BuildContext context) {
    final fullName = context.watch(
      userRef.select((user) => '${user.firstName} ${user.lastName}'),
    );
    return Text(fullName);
  }
}

Consumer

If you want to rebuild only a part of your widget tree and don't want to create a new widget, you can use the Consumer widget. This widget can take a watchable (a StateRef or even a selected state of a StateRef).

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

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Consumer(
        watchable:
            userRef.select((user) => '${user.firstName} ${user.lastName}'),
        builder: (context, String fullName, child) => Text(fullName),
      ),
    );
  }
}

LogicLoader

If you want to trigger an asynchronous data load of a logic, from the widget side, LogicLoader is the widget you need!

To use it, you have to implement the Loadable interface in the logic which needs to load data. Then you'll have to override the load method and fetch the data inside it.

final usersRef = StateRef(const <User>[]);
final loadingRef = StateRef(false);

final usersLogicRef = LogicRef((scope) => UsersLogic(scope));

class UsersLogic with Logic implements Loadable {
  const UsersLogic(this.scope);

  @override
  final Scope scope;

  UsersRepository get _usersRepository => use(usersRepositoryRef);

  @override
  Future<void> load() async {
    write(loadingRef, true);
    final users = await _usersRepository.fetchAll();
    write(usersRef, users);
    write(loadingRef, false);
  }
}

From the widget side, you'll have to use the LogicLoader and provide it the logic references you want to load:

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

  @override
  Widget build(BuildContext context) {
    return LogicLoader(
      refs: [usersLogicRef],
      child: const UsersView(),
    );
  }
}

You can watch the state in a subtree to display a progress indicator when the data is fetching:

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

  @override
  Widget build(BuildContext context) {
    final loading = context.watch(loadingRef);
    if (loading) {
      return const CircularProgressIndicator();
    }

    // Display the users in a list when have been fetched.
    final users = context.watch(usersRef);
    return ListView(...);
  }
}

Alternatively, you can use the builder parameter to achieve the same goal:

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

  @override
  Widget build(BuildContext context) {
    return LogicLoader(
      refs: [usersLogicRef],
      builder: (context, loading, child) {
        if (loading) {
          return const CircularProgressIndicator();
        }

        // Display the users in a list when have been fetched.
        final users = context.watch(usersRef);
        return ListView();
      },
    );
  }
}

Overrides

It can be useful to be able to override the initial state of StateRef or the factory of LogicRef in some conditions:

  • When we want a subtree to have its own state/logic under the same reference.
  • For mocking values in tests.
Reusing a reference under a different scope.

Let's say we want to create an app where a user can create counters and see the sum of all counters:

Counters

We could do this by having a global state being a list of integers, and a business logic component for adding counters and increment them:

final countersRef = StateRef(const <int>[]);

final countersLogic = LogicRef((scope) => CountersLogic(scope));

class CountersLogic with Logic {
  const CountersLogic(this.scope);

  @override
  final Scope scope;

  void addCounter() {
    write(countersRef, read(countersRef).toList()..add(0));
  }

  void increment(int index) {
    final counters = read(countersRef).toList();
    counters[index]++;
    write(countersRef, counters);
  }
}

We can then use the select extension method in a widget to watch the sum of this list:

final sum = context.watch(countersRef.select(
  (counters) => counters.fold<int>(0, (a, b) => a + b),
));

Now, for creating the counter view, we can have an index parameter in the constructor of this view. This has some drawbacks:

  • If a child widget needs to access this index, we would need to pass the index for every widget down the tree, up to our child.
  • We cannot use the const keyword anymore.

A better approach would be to create a BinderScope above each counter widget. We would then configure this BinderScope to override the state of a StateRef for its descendants, with a different initial value.

Any StateRef or LogicRef can be overriden in a BinderScope. When looking for the current state, a descendant will get the state of the first reference overriden in a BinderScope until the root BinderScope. This can be written like this:

final indexRef = StateRef(0);

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final countersCount =
        context.watch(countersRef.select((counters) => counters.length));

    return Scaffold(
     ...
      child: GridView(
        ...
        children: [
          for (int i = 0; i < countersCount; i++)
            BinderScope(
              overrides: [indexRef.overrideWith(i)],
              child: const CounterView(),
            ),
        ],
      ),
     ...
    );
  }
}

The BinderScope constructor has an overrides parameter which can be supplied from an overrideWith method on StateRef and LogicRef instances.

Note: The whole code for the above snippets is available in the example file.

Mocking values in tests

Let's say you have an api client in your app:

final apiClientRef = LogicRef((scope) => ApiClient());

If you want to provide a mock instead, while testing, you can do:

testWidgets('Test your view by mocking the api client', (tester) async {
  final mockApiClient = MockApiClient();

  // Build our app and trigger a frame.
  await tester.pumpWidget(
    BinderScope(
      overrides: [apiClientRef.overrideWith((scope) => mockApiClient)],
      child: const MyApp(),
    ),
  );

  expect(...);
});

Whenever the apiClientRef is used in your app, the MockApiClient instance will be used instead of the real one.


Advanced usage

Computed

You may encounter a situation where different widgets are interested in a derived state which is computed from different sates. In this situation it can be helpful to have a way to define this derived state globally, so that you don't have to copy/paste this logic across your widgets. Binder comes with a Computed class to help you with that use case.

Let's say you have a list of products referenced by productsRef, each product has a price, and you can filter these products according to a price range (referenced by minPriceRef and maxPriceRef).

You could then define the following Computed instance:

final filteredProductsRef = Computed((watch) {
  final products = watch(productsRef);
  final minPrice = watch(minPriceRef);
  final maxPrice = watch(maxPriceRef);

  return products
      .where((p) => p.price >= minPrice && p.price <= maxPrice)
      .toList();
});

Like StateRef you can watch a Computed in the build method of a widget:

@override
Widget build(BuildContext context) {
  final filteredProducts = context.watch(filteredProductsRef);
  ...
  // Do something with `filteredProducts`.
}

Note: The whole code for the above snippets is available in the example file.

Observers

You may want to observe when the state changed and do some action accordingly (for example, logging state changes). To do so, you'll need to implement the StateObserver interface (or use a DelegatingStateObserver) and provide an instance to the observers parameter of the BinderScope constructor.

bool onStateUpdated<T>(StateRef<T> ref, T oldState, T newState, Object action) {
  logs.add(
    '[${ref.key.name}#$action] changed from $oldState to $newState',
  );

  // Indicates whether this observer handled the changes.
  // If true, then other observers are not called.
  return true;
}
...
BinderScope(
  observers: [DelegatingStateObserver(onStateUpdated)],
  child: const SubTree(),
);

Undo/Redo

Binder comes with a built-in way to move in the timeline of the state changes. To be able to undo/redo a state change, you must add a MementoScope in your tree. The MementoScope will be able to observe all changes made below it:

return MementoScope(
  child: Builder(builder: (context) {
    return MaterialApp(
      home: const MyHomePage(),
    );
  }),
);

Then, in a business logic, stored below the MementoScope, you will be able to call undo/redo methods.

Note: You will get an AssertionError at runtime if you don't provide a MementoScope above the business logic calling undo/redo.

Disposable

In some situation, you'll want to do some action before the BinderScope hosting a business logic component, is disposed. To have the chance to do this, your logic will need to implement the Disposable interface.

class MyLogic with Logic implements Disposable {
  void dispose(){
    // Do some stuff before this logic go away.
  }
}

StateListener

If you want to navigate to another screen or show a dialog when a state change, you can use the StateListener widget.

For example, in an authentication view, you may want to show an alert dialog when the authentication failed. To do it, in the logic component you could set a state indicating whether the authentication succeeded or not, and have a StateListener in your view do respond to these state changes:

return StateListener(
  watchable: authenticationResultRef,
  onStateChanged: (context, AuthenticationResult state) {
    if (state is AuthenticationFailure) {
      showDialog<void>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: const Text('Error'),
            content: const Text('Authentication failed'),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('Ok'),
              ),
            ],
          );
        },
      );
    } else {
      Navigator.of(context).pushReplacementNamed(route_names.home);
    }
  },
  child: child,
);

In the above snippet, each time the state referenced by authenticationResultRef changes, the onStateChanged callback is fired. In this callback we simply verify the type of the state to determine whether we have to show an alert dialog or not.

DartDev Tools

Binder wants to simplify the debugging of your app. By using the DartDev tools, you will be able to inspect the current states hosted by any BinderScope.


Snippets

You can find code snippets for vscode at snippets.

Sponsoring

I'm working on my packages on my free-time, but I don't have as much time as I would. If this package or any other package I created is helping you, please consider to sponsor me so that I can take time to read the issues, fix bugs, merge pull requests and add features to these packages.

Contributions

Feel free to contribute to this project.

If you find a bug or want a feature, but don't know how to fix/implement it, please fill an issue.
If you fixed a bug or implemented a feature, please send a pull request.

Comments
  • Few questions regarding best practices using the

    Few questions regarding best practices using the "binder" library

    Hi!

    Let's get straight to the point: I fell in love with your library. The architecture separating view/logic/state that you set up is what I have always aimed for but never managed to achieve. You succeeded brilliantly in establishing a simple but complete design.

    Before that I've done countless refactors of my application. I've tried different approaches without ever finding one that fully satisfied me. Finally, I stumbled upon your creation last week by coincidence. I decided to give it a try for yet another refactor... It has been an incredible pleasure. This solved all the difficulties, trade-offs and accidental complexities I had been facing until then. My code is so much more modular, it's a breath of fresh air. My views are much less nested. It is infinitely simpler to share the same state between several business and UI components. I love how simple and elegant it is, and I love how development just has to let itself be guided by your api.

    In short: state management in Flutter is a solved problem thanks to binder.


    While I was re-writing my application, I noted a few points of interest that I would have liked to clarify. I hope you don't mind if I ask you about some of these. A short answer will suffice, I don't want to abuse your time. It's simply that I want to make the best use of your library.

    Is there any purpose for the public scope attribute of a Logic? Each class inheriting Logic has a public Scope. I understand what it stands for, but is there any good reason to directly access this object instead of calling use()/read()?

    What happens if I initialize a reference inside a widget? All the examples show the use of LogicRef and StateRef being global variables. I was wondering: is it a problem if one is initialized as a widget attribute? There is no risk of memory leak or callback called several times?

    Is there any reason why it is not possible to call context.read() for a Computed? In particular, if one wants to access the value in a callback. It's possible to use context.read() on a StateRef but not on a Computed (which I thought would be similar to a StateRef as it is derived from). Why that?

    What is the best way to request data while building a widget? Sometimes, my View or Widget does not depend on an actual state but rather on temporal data. For example, when I need to display a list of values loaded from the cloud. How do you usually load and store this ephemeral data from a LogicRef? I do something similar to this:

    FutureBuilder(
        future: Future.microtask(() => context.use(myLogicRef).loadData()),
        builder: (context, snapshot) => ...,
    )
    

    Is this an acceptable workaround, or does it indicate a problem in the structure of my application?

    How to handle the lifetime of one or more Logic "instances"? So, let's say inside my application there is a chess game. I implemented it using one ChessGameLogic which involves many states: chessBoardRef, blunderCountRef, turnRef, elapsedTimeRef, isGameFinishedRef, winnerRef, etc. So anytime the user navigates to the /chess page a new game must be started. By starting a new game, all previously listed states need to be reset to their initial value. When the game ends and is not used by any view, the refs can be disposed. What would be the best way to use binder in this case?

    I could declare an init() function in my logic and make my view call it before starting the game. However, if my user is very strong, I want to display two games at the same time for him to play them simultaneously. That means I need two separate sets of states. Is it a good use case for overrideWith()?

    I was thinking of declaring a function inside the logic file like that (note that I'm overriding the logic reference too):

    List<BinderOverride> createGameInstance() => [
        chessGameLogicRef.overrideWith((scope) => ChessGameLogic(scope)),
        chessBoardRef.overrideWith(ChessBoard()),
        blunderCountRef.overrideWith(0),
        ...
    ]
    

    And declare a new BinderScope in the view file:

    BinderScope(
        overrides: createGameInstance(),
        child: GameChessView(),
    )
    

    Would you say that this is the idiomatic way to handle the Logic as if it were a class instance? I mean, the logic and states do not actually need to exist by default. They can be instantiated independently one or multiple times, and they should "die" when the view no longer use it.

    Well, that's it for tonight. :) Anyway, thank you very much for the work you did!

    question 
    opened by Delgan 9
  • load first event from stream

    load first event from stream

    I have several Logic classes which subscribes to one ore more steams. I use Loadable so i can use them with LogicLoader, but i would like to get the first stream event before returning (so that values are not always null at first) and also subscribing to new events. How would you do this? I read that if i first listen to stream.first then non-broadcast streams are closed it says, however the following seem to work (although a bit awkward):

      @override
      Future<void> load() async {
        final User user = await userStream.first;
        write(userRef, user);
        _userSubscription = _userSubscription = userStream.listen((User user) {
          write(userRef, user);
        });
      }
    
    opened by erf 6
  • Suggestion: naming conventions

    Suggestion: naming conventions

    I see in the examples the use of counterRef and i wonder if the name counterState would better describe the state objects as a convention - that is ending with State instead of Ref.

    Also counterViewLogicRef could perhaps be named counterViewLogic as a more compact name.

    Both naming conventions would then match nicely with the holy trinity of View-Logic-State

    What do you think?

    opened by erf 6
  • Architecture example failing to run

    Architecture example failing to run

    flutter run

    lib/data/entities/user.dart:4:6: Error: Error when reading 'lib/data/entities/user.freezed.dart': No such file or directory
    part 'user.freezed.dart';                                               
         ^                                                                  
    lib/data/entities/user.dart:5:6: Error: Error when reading 'lib/data/entities/user.g.dart': No such file or directory
    part 'user.g.dart';                                                     
         ^                                                                  
    lib/data/entities/user.dart:4:6: Error: Can't use 'lib/data/entities/user.freezed.dart' as a part, because it has no 'part of' declaration.
    part 'user.freezed.dart';                                               
         ^                                                                  
    lib/data/entities/user.dart:5:6: Error: Can't use 'lib/data/entities/user.g.dart' as a part, because it has no 'part of' declaration.
    part 'user.g.dart';                                                     
         ^                                                                  
    lib/data/entities/user.dart:8:26: Error: Type '_$User' not found.       
    abstract class User with _$User {                                       
                             ^^^^^^                                         
    lib/data/entities/user.dart:8:16: Error: The type '_$User' can't be mixed in.
    abstract class User with _$User {                                       
                   ^                                                        
    lib/data/entities/user.dart:16:8: Error: Couldn't find constructor '_User'.
      }) = _User;                                                           
           ^                                                                
    lib/data/entities/user.dart:16:8: Error: Redirection constructor target not found: '_User'
      }) = _User;                                                           
           ^                                                                
    lib/modules/home/view.dart:76:69: Error: The getter 'name' isn't defined for the class 'User'.
     - 'User' is from 'package:architecture/data/entities/user.dart' ('lib/data/entities/user.dart').
    Try correcting the name to the name of an existing getter, or defining a getter or field named 'name'.
        final name = context.watch(currentUserRef.select((user) => user.name));
                                                                        ^^^^
    lib/modules/user/view.dart:25:69: Error: The getter 'name' isn't defined for the class 'User'.
     - 'User' is from 'package:architecture/data/entities/user.dart' ('lib/data/entities/user.dart').
    Try correcting the name to the name of an existing getter, or defining a getter or field named 'name'.
        final name = context.watch(currentUserRef.select((user) => user.name));
                                                                        ^^^^
    lib/data/entities/user.dart:18:55: Error: Method not found: '_$UserFromJson'.
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
                                                          ^^^^^^^^^^^^^^    
    lib/data/sources/api_client.dart:42:52: Error: The method 'toJson' isn't defined for the class 'User'.
     - 'User' is from 'package:architecture/data/entities/user.dart' ('lib/data/entities/user.dart').
    Try correcting the name to the name of an existing method, or defining a method named 'toJson'.
      Map<String, dynamic> encode(User value) => value.toJson();            
                                                       ^^^^^^               
                                                                            
                                                                            
    FAILURE: Build failed with an exception.                                
                                                                            
    * Where:                                                                
    Script '/home/mitesh/fvm/versions/stable/packages/flutter_tools/gradle/flutter.gradle' line: 904
                                                                            
    * What went wrong:                                                      
    Execution failed for task ':app:compileFlutterBuildDebug'.              
    > Process 'command '/home/mitesh/fvm/versions/stable/bin/flutter'' finished with non-zero exit value 1
                                                                            
    * Try:                                                                  
    Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
                                                                            
    * Get more help at https://help.gradle.org                              
                                                                            
    BUILD FAILED in 23s                                                     
    Running Gradle task 'assembleDebug'...                                  
    Running Gradle task 'assembleDebug'... Done                        23.8s
    Exception: Gradle task assembleDebug failed with exit code 1
    
    
    opened by isinghmitesh 6
  • compiler problems

    compiler problems

    I'm both a flutter and binder newbie, so please go easy if this is a stupid question ;)

    I'm playing around with the examples , and I'm just wondering how to make this warning go away ..

    image

    the code in question is

    image

    as you can see, the "watch" is underlined in blue

    There are also several deprecated notices - once I get the examples running I can try and get those fixed and submit a PR if that's useful ?

    opened by jmls 5
  • The method 'write' was called on null

    The method 'write' was called on null

    I made a simple example and i fail to write a state on a button press.

    Did i do something silly?

    flutter --version

    Flutter 2.1.0-12.2.pre • channel beta • https://github.com/flutter/flutter.git
    Framework • revision 5bedb7b1d5 (10 days ago) • 2021-03-17 17:06:30 -0700
    Engine • revision 711ab3fda0
    Tools • Dart 2.13.0 (build 2.13.0-116.0.dev)
    

    I use binder version ^0.4.0

    And i used the following environment (default from flutter create):

    environment:
      sdk: ">=2.7.0 <3.0.0"
    

    Example:

    import 'package:flutter/material.dart';
    import 'package:binder/binder.dart';
    
    final testRef = StateRef(100);
    
    final testLogicViewRef = LogicRef((scope) => TestLogic(scope));
    
    class TestLogic with Logic {
      TestLogic(Scope scope);
    
      @override
      Scope scope;
    
      void writeState(int val) {
        write(testRef, val);
      }
    }
    
    void main() {
      runApp(BinderScope(child: MyApp()));
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Binder test'),
          ),
          body: Center(
            child: Column(
              children: [
                Text(context.watch(testRef).toString()),
                TextButton(
                  child: Text('Test'),
                  onPressed: () {
                    context.use(testLogicViewRef).writeState(999);
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    

    I get the following error:

    Restarted application in 327ms.
    
    ════════ Exception caught by gesture ═══════════════════════════════════════════
    The following NoSuchMethodError was thrown while handling a gesture:
    The method 'write' was called on null.
    Receiver: null
    Tried calling: write<int>(Instance of 'StateRef<int>', 999, null)
    
    When the exception was thrown, this was the stack
    #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:54:5)
    #1      Logic.write
    #2      TestLogic.writeState
    #3      MyHomePage.build.<anonymous closure>
    #4      _InkResponseState._handleTap
    ...
    Handler: "onTap"
    Recognizer: TapGestureRecognizer#e2765
        debugOwner: GestureDetector
        state: possible
        won arena
        finalPosition: Offset(192.0, 106.0)
        finalLocalPosition: Offset(36.5, 7.0)
        button: 1
        sent tap down
    ════════════════════════════════════════════════════════════════════════════════
    
    opened by erf 3
  • async calls in Computed

    async calls in Computed

    One of the great things about Riverpod, is that you can use Providers to listen to and transform state from one to another. E.g. i transform a Firebase User to my custom AppUser, which requires a set of async calls to check if the user already exists in Firestore. It seem Computed is similar to Providers in this sense, but it seem i'm not allowed to add the async keyword to the Computed function. Is this something that could be changed or is there another way to solve combining states with async calls using binder?

    Thanks for a VERY promising package! :)

    opened by erf 3
  • type 'String' is not a subtype of type 'Null' in type cast

    type 'String' is not a subtype of type 'Null' in type cast

    If i try to write a String to a StateRef which was initialized with a null value and without type declared, i get the following exception:

    Restarted application in 302ms.
    
    ════════ Exception caught by widgets library ═══════════════════════════════════
    The following _CastError was thrown building BinderScope(overrides: [], observers: [], state: BinderScopeState#a3935(LogicRef<StateTest>: Instance of 'StateTest', StateRef<Null>: HELLO)):
    type 'String' is not a subtype of type 'Null' in type cast
    
    The relevant error-causing widget was
    BinderScope
    When the exception was thrown, this was the stack
    #0      BinderContainerMixin.fetch
    #1      StateRef.read
    #2      Aspect.shouldRebuild
    #3      InheritedBinderScope.updateShouldNotifyDependent.<anonymous closure>
    #4      Iterable.any (dart:core/iterable.dart:356:15)
    ...
    ════════════════════════════════════════════════════════════════════════════════
    
    

    Here is the failing example:

    import 'package:flutter/material.dart';
    import 'package:binder/binder.dart';
    
    final stateRef = StateRef(null);
    
    final stateTestRef = LogicRef((scope) => StateTest(scope));
    
    class StateTest with Logic {
      const StateTest(this.scope);
    
      @override
      final Scope scope;
    
      void sayHello() {
        write(stateRef, 'HELLO');
      }
    }
    
    void main() {
      runApp(BinderScope(child: MyApp()));
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Binder test'),
          ),
          body: Center(
            child: Column(
              children: [
                Text(context.watch(stateRef) ?? ''),
                TextButton(
                  child: Text('Say hello'),
                  onPressed: () {
                    context.use(stateTestRef).sayHello();
                  },
                )
              ],
            ),
          ),
        );
      }
    }
    
    

    If i set the type of the StateRef like below, it works ok:

    final stateRef = StateRef<String>(null);
    

    Is this expected behavior?

    Maybe related to: https://github.com/letsar/binder/issues/8

    opened by erf 2
  • Uppercase LogicRef when using vscode extension

    Uppercase LogicRef when using vscode extension

    When using the Logic snippet I noticed that the refs are capitalized. Take the following example.

    // Should be counterViewLogicRef
    final CounterViewLogicRef = LogicRef((scope) => CounterViewLogic(scope));
    

    It may be better to have the first tab be for the ref variable and the second to be for the class name.

    opened by chimon2000 2
  • Widget watching on Computed is sometimes not rebuilt

    Widget watching on Computed is sometimes not rebuilt

    Hey @letsar!

    So, I was doing some tests using your excellent package, and I noticed a very weird bug. Basically, I have one widget watching a Computed reference which itself is based on a State reference. Each time the button is pressed, the state is updated and the other reference is re-computed. So far so good.

    However, the widget is not systematically rebuilt. It only works half the time, randomly. Yet, the value returned by the Computed is definitely different!

    https://user-images.githubusercontent.com/4193924/120108355-1f8b1b80-c165-11eb-8f40-6865a460d624.mp4

    import 'package:binder/binder.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    
    void main() => runApp(BinderScope(child: AppView()));
    
    class AppView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              RaisedButton(
                child: Text("Press me"),
                onPressed: () => context.use(testLogicRef).click(),
              ),
              Builder(
                builder: (context) {
                  final val = context.watch(computedValRef);
                  print("Rebuilt with: $val");
                  return Text("$val");
                },
              )
            ],
          ),
        );
      }
    }
    
    final testLogicRef = LogicRef((scope) => TestLogic(scope));
    
    final testStateRef = StateRef(0);
    
    final computedValRef = Computed<int>((watch) {
      final val = watch(testStateRef);
      final computedVal = DateTime.now().millisecondsSinceEpoch;
      print("Computed value: $computedVal");
      return computedVal;
    });
    
    class TestLogic with Logic {
      const TestLogic(this.scope);
    
      @override
      final Scope scope;
    
      void click() => update(testStateRef, (int val) => val + 1);
    }
    

    It drives me crazy. Am I doing something wrong?

    Also, note that when I press the button once, the Computed is called twice or thrice. Is that expected?

    I'm using Binder 0.4.0 with Flutter 2.0.6 and Dart 2.12.3.

    opened by Delgan 7
  • `Scope.write()` without explicit type argument is not compile time safe

    `Scope.write()` without explicit type argument is not compile time safe

    When there's no explicit type argument on Scope.write(), it is compile time valid to pass a state that can't be attributed to ref. Take this example:

    final countRef = StateRef(0);
    
    class CounterLogic with Logic {
      ...
      void increment() {
        write(countRef, 'banana'); // compiler: yup, go ahead.
      }
    }
    

    This happens because, implicitly, T becomes the least upper bound type of both arguments, which is Object. Such conclusion implies that, more precisely, the problem is not about explicitly defining a type argument, since write<Object>(countRef, 'banana') is also compile time valid. The actual point is the way the compiler fails to spot this particular incorrect construction.

    The only way I see to solve this up is by not declaring both state and ref as parameters of the same method. I'd consider something like the following:

    abstract class Scope {
      StateRefHandler<T> onStateRef<T>(StateRef<T> ref);
      ...
    }
    
    abstract class StateRefHandler<T> {
      void write<T>(T state, [Object action]);
      ...
    }
    

    There's a clear complexity increase on both library implementation and use, besides being a breaking change, but I believe it's worth it.

    An alternative is discouraging calling Scope.write() without explicit type arguments. Perhaps a summary of this discussion could be placed somewhere at the documentation.

    enhancement 
    opened by hcbpassos 5
  • Calling

    Calling "Logic.write()" during "initState()" leads to error

    Hi again!

    This is a continuation of the discussion that took place in #2.

    I put back here the offending code:

    final fetchedCountRef = StateRef(0);
    
    final dataFetcherRef = LogicRef((scope) => DataFetcher(scope));
    
    class DataFetcher with Logic {
      const DataFetcher(this.scope);
    
      @override
      final Scope scope;
    
      int fetch(int num) {
        update<int>(fetchedCountRef, (count) => count + 1);
        return num * 2;
      }
    }
    
    class MyData extends StatefulWidget {
      final int num;
    
      const MyData(this.num);
    
      @override
      _MyDataState createState() => _MyDataState();
    }
    
    class _MyDataState extends State<MyData> {
      int data;
    
      @override
      void initState() {
        super.initState();
        data = context.use(dataFetcherRef).fetch(widget.num);
      }
    
      @override
      Widget build(BuildContext context) => Text("Fetched data: $data");
    }
    

    Updating fetchedCountRef forces a rebuild, but this is not possible during initState() and raises an exception.

    ════════ Exception caught by widgets library ════════ The following assertion was thrown building _BodyBuilder: setState() or markNeedsBuild() called during build.

    This BinderScope 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.

    I have (yet another) question: the documentation about context.use() states that it "cannot be called while building a widget". It's a little bit ambiguous for me: does it includes the initState() phase too?

    From what I see in the source examples, you are systematically calling addPostFrameCallback() while using a Logic from initState(). Also, not using addPostFrameCallback() can cause subtle errors as reported by this ticket. Should context.use() be forbidden inside initState()?

    Also, in #2 I said that as a workaround I would probably call Future.microtask(() => update(...)); in the Logic but finally I'm discarding this solution. :smile: It can lead to subtle bugs if another logic doesn't expect the change to be asynchronous. In addition, I prefer for the the logic to be decoupled from the view as much as possible.

    So, I think I'm back to the FutureBuilder() solution that I initially used, but storing the Future as you suggested.

    class _MyDataState extends State<MyData> {
      Future<int> data;
    
      @override
      void initState() {
        super.initState();
        data = Future.microtask(() => context.use(dataFetcherRef).fetch(widget.num));
      }
    
      @override
      Widget build(BuildContext context) => FutureBuilder(
            future: data,
            builder: (context, snapshot) => Text("Fetched data: ${snapshot.data ?? '?'}"),
          );
    }
    

    It's a bit inconvenient as it requires handling invalid data and forces the widget to be immediately rebuilt. However that's the solution I prefer. Additionally, I will refrain myself from directly using context.use() in any initState() method as I'm afraid of inadvertently breaking a widget while modifying a Logic method.

    I don't know if other ideas will come to your mind. Sadly, it seems not possible to "fix" this internally without changing the binder api. It is understandable that you prefer not to extend the api for this particular use case. I have been thinking to a possible workaround, I wonder if it can be implemented as an extension. :thinking:

    data = context.borrow(dataFetcherRef, (ref) => ref.fetch(widget.num));
    

    The idea would be to allow borrow() only inside initState(). The callback would be executed immediately, but first the scope would be somehow "marked" so that each call to setState() would be automatically postponed using addPostFrameCallback(). Then the scope would return to normal state.

    In any case, this is as far as my knowledge goes. Thank you again for having looked into my problem! Hopefully, an elegant solution will eventually be found.

    opened by Delgan 5
Owner
Romain Rastel
Flutter Developer
Romain Rastel
MobX for the Dart language. Hassle-free, reactive state-management for your Dart and Flutter apps.

Language: English | Português | Chinese mobx.dart MobX for the Dart language. Supercharge the state-management in your Dart apps with Transparent Func

MobX 2.2k Dec 27, 2022
Manage the state of your widgets using imperative programming concepts.

Imperative Flutter Manage the state of your widgets using imperative programming concepts. Setup Intall imperative_flutter package in pubspec.yaml dep

Jeovane Santos 5 Aug 20, 2022
A predictable state management library that helps implement the BLoC design pattern

A predictable state management library that helps implement the BLoC design pattern. Package Pub bloc bloc_test flutter_bloc angular_bloc hydrated_blo

Felix Angelov 9.9k Dec 31, 2022
Another state management solution

VxState VxState is a state management library built for Flutter apps with focus on simplicity. It is inspired by StoreKeeper & libraries like Redux, V

Pawan Kumar 42 Dec 24, 2022
A flutter boilerplate project with GetX state management.

flutter_getx_boilerplate Languages: English (this file), Chinese. Introduction During my study of Flutter, I have been using the flutter_bloc state ma

Kevin Zhang 234 Jan 5, 2023
Flutter MVU architecture/state management package

mvu_flutter No mutability. No builders. No connectors. No reducers. No StreamControllers and subscription management. A truly declarative state manage

Yakov Karpov 7 Jul 29, 2021
Simple global state management for Flutter

Slices Slices is a minimalist state manegement, focused specifically for applications that needs a global state where different "pieces" of the applic

Erick 5 Jun 15, 2021
The modular state management solution for flutter.

The modular state management solution for flutter. Easy debugging : each event is predictable and goes into a single pipeline Centralized state : soli

Aloïs Deniel 44 Jul 6, 2022
Example of use bloc + freezed with a state that contains a list

blocfreezedlistexample A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you s

Leynier Gutiérrez González 2 Mar 21, 2022
London App Brewery State Management Project

todey_flutter A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you starte

null 0 Nov 1, 2021
Timer based on provider state manager

timer_provider Timer based on provider state manager Getting Started This project is a starting point for a Flutter application. A few resources to ge

Елизавета Лободина 0 Nov 6, 2021
Flutter State Management with provider :rocket:

Flutter - Gerenciamento de Estados com Provider Objetivos ao completar os estudos Aprenda a gerenciar o estado da sua aplicação com Single Source of T

Tiago Barbosa 0 Dec 6, 2021
Trip management mobile Application

HereYouGO Trip management mobile Application This app will help you Track your expense during your trips. Track your trip destinations and the sub tri

Krupali Mehta 4 Jul 7, 2022
A powerful Flutter chat UI component library and business logic for Tencent Cloud Chat, creating seamless in-app chat modules for delightful user experiences.

<style> .button-9 { appearance: button; backface-visibility: hidden; background-color: #1d52d9; border-radius: 6px; border-width: 0; box-shadow: rgba(

Tencent Cloud 63 Aug 11, 2023
A state management library that enables concise, fluid, readable and testable business logic code.

Creator is a state management library that enables concise, fluid, readable, and testable business logic code. Read and update state with compile time

Xianzhe Liang 181 Dec 24, 2022
MV* Architectures - Responsible for the business logic of the application.

MV* Architectures MVC Model Responsible for the business logic of the application. Persisting application state: communication with the database. Data

Mekni_Wassime 2 Mar 15, 2022
Generic validator - A generic validator with business logic separation in mind

This package provides APIs to facilitate separating validation and business rule

Ahmed Aboelyazeed 5 Jan 3, 2023
Learn how to build a multi-step form flow and how to use bloc to effectively isolate the presentation layer from the business logic layer.

Multi-page Form Flow Learn how to build a multi-step form flow and how to use bloc to effectively isolate the presentation layer from the business log

Sandip Pramanik 15 Dec 19, 2022
Flutter-business-card-app - Flutter + Dart business card mobile app

Dart + Flutter Business Card Mobile Application

Mark Hellner 1 Nov 8, 2022
Business Card - Business Card App Built With Flutter

Business Card app. Basic single page app with functionality. For now you can cha

buraktaha 5 Apr 20, 2022