{"id":19775004,"url":"https://github.com/letsar/binder","last_synced_at":"2025-04-30T19:30:23.037Z","repository":{"id":39583272,"uuid":"306742280","full_name":"letsar/binder","owner":"letsar","description":"A lightweight, yet powerful way to bind your application state with your business logic.","archived":false,"fork":false,"pushed_at":"2022-08-08T17:07:08.000Z","size":2668,"stargazers_count":179,"open_issues_count":4,"forks_count":12,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-06T04:06:31.960Z","etag":null,"topics":["flutter","inheritedwidget","service-locator","state-management"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/letsar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"letsar","patreon":"romainrastel","open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":["https://www.buymeacoffee.com/romainrastel","paypal.me/RomainRastel"]}},"created_at":"2020-10-23T20:38:48.000Z","updated_at":"2025-02-20T00:53:54.000Z","dependencies_parsed_at":"2022-09-20T22:01:55.616Z","dependency_job_id":null,"html_url":"https://github.com/letsar/binder","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letsar%2Fbinder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letsar%2Fbinder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letsar%2Fbinder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/letsar%2Fbinder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/letsar","download_url":"https://codeload.github.com/letsar/binder/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251769149,"owners_count":21640846,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flutter","inheritedwidget","service-locator","state-management"],"created_at":"2024-11-12T05:14:33.201Z","updated_at":"2025-04-30T19:30:21.011Z","avatar_url":"https://github.com/letsar.png","language":"Dart","readme":"[![Build][github_action_badge]][github_action]\n[![Pub][pub_badge]][pub]\n[![Codecov][codecov_badge]][codecov]\n\n# binder\n\n![Logo][binder_logo]\n\nA lightweight, yet powerful way to bind your application state with your business logic.\n\n## The vision\n\nAs other state management pattern, **binder** aims to separate the **application state** from the **business logic** that updates it:\n\n![Data flow][img_data_flow]\n\nWe can see the whole application state as the agglomeration of a multitude of tiny states. Each state being independent from each other.\nA view can be interested in some particular states and has to use a logic component to update them.\n\n\n\n- [binder](#binder)\n  - [The vision](#the-vision)\n  - [Getting started](#getting-started)\n    - [Installation](#installation)\n    - [Basic usage](#basic-usage)\n    - [Intermediate usage](#intermediate-usage)\n      - [Select](#select)\n      - [Consumer](#consumer)\n      - [LogicLoader](#logicloader)\n      - [Overrides](#overrides)\n        - [Reusing a reference under a different scope.](#reusing-a-reference-under-a-different-scope)\n      - [Mocking values in tests](#mocking-values-in-tests)\n    - [Advanced usage](#advanced-usage)\n      - [Computed](#computed)\n      - [Observers](#observers)\n      - [Undo/Redo](#undoredo)\n      - [Disposable](#disposable)\n      - [StateListener](#statelistener)\n      - [DartDev Tools](#dartdev-tools)\n  - [Snippets](#snippets)\n  - [Sponsoring](#sponsoring)\n  - [Contributions](#contributions)\n\n## Getting started\n\n### Installation\n\nIn the `pubspec.yaml` of your flutter project, add the following dependency:\n\n```yaml\ndependencies:\n  binder: \u003clatest_version\u003e\n```\n\nIn your library add the following import:\n\n```dart\nimport 'package:binder/binder.dart';\n```\n\n***\n\n### Basic usage\n\nAny state has to be declared through a `StateRef` with its initial value: \n\n```dart\nfinal counterRef = StateRef(0);\n```\n\n**Note**: A state should be immutable, so that the only way to update it, is through methods provided by this package.\n\nAny logic component has to be declared through a `LogicRef` with a function that will be used to create it:\n\n```dart\nfinal counterViewLogicRef = LogicRef((scope) =\u003e CounterViewLogic(scope));\n```\n\nThe `scope` argument can then be used by the logic to mutate the state and access other logic components.\n\n**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.\n\nIf we want our `CounterViewLogic` to be able to increment our counter state, we might write something like this:\n\n```dart\n/// A business logic component can apply the [Logic] mixin to have access to\n/// useful methods, such as `write` and `read`.\nclass CounterViewLogic with Logic {\n  const CounterViewLogic(this.scope);\n\n  /// This is the object which is able to interact with other components.\n  @override\n  final Scope scope;\n\n  /// We can use the [write] method to mutate the state referenced by a\n  /// [StateRef] and [read] to obtain its current state.\n  void increment() =\u003e write(counterRef, read(counterRef) + 1);\n}\n```\n\nIn order to bind all of this together in a Flutter app, we have to use a dedicated widget called `BinderScope`.\nThis widget is responsible for holding a part of the application state and for providing the logic components.\nYou will typically create this widget above the `MaterialApp` widget:\n\n```dart\nBinderScope(\n  child: MaterialApp(\n    home: CounterView(),\n  ),\n);\n```\n\nIn 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:\n\n```dart\n\nclass CounterView extends StatelessWidget {\n  const CounterView({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    /// We call the [watch] extension method on a [StateRef] to rebuild the\n    /// widget when the underlaying state changes.\n    final counter = context.watch(counterRef);\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Binder example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: \u003cWidget\u003e[\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: Theme.of(context).textTheme.headline4),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        /// We call the [use] extension method to get a business logic component\n        /// and call the appropriate method.\n        onPressed: () =\u003e context.use(counterViewLogicRef).increment(),\n        tooltip: 'Increment',\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n```\n\nThis is all you need to know for a basic usage.\n\n**Note**: The whole code for the above snippets is available in the [example][example_main] file.\n\n***\n\n### Intermediate usage\n\n#### Select\n\nA state can be of a simple type as an `int` or a `String` but it can also be more complex, such as the following:\n\n```dart\nclass User {\n  const User(this.firstName, this.lastName, this.score);\n\n  final String firstName;\n  final String lastName;\n  final int score;\n}\n```\n\nSome 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.\n\nFor 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:\n\n```dart\nclass AppBarTitle extends StatelessWidget {\n  const AppBarTitle({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final fullName = context.watch(\n      userRef.select((user) =\u003e '${user.firstName} ${user.lastName}'),\n    );\n    return Text(fullName);\n  }\n}\n```\n\n#### Consumer\n\nIf 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.\nThis widget can take a watchable (a `StateRef` or even a selected state of a `StateRef`).\n\n```dart\nclass MyAppBar extends StatelessWidget {\n  const MyAppBar({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBar(\n      title: Consumer(\n        watchable:\n            userRef.select((user) =\u003e '${user.firstName} ${user.lastName}'),\n        builder: (context, String fullName, child) =\u003e Text(fullName),\n      ),\n    );\n  }\n}\n```\n\n#### LogicLoader\n\nIf you want to trigger an asynchronous data load of a logic, from the widget side, `LogicLoader` is the widget you need! \n\nTo use it, you have to implement the `Loadable` interface in the logic which needs to load data.\nThen you'll have to override the `load` method and fetch the data inside it.\n\n```dart\nfinal usersRef = StateRef(const \u003cUser\u003e[]);\nfinal loadingRef = StateRef(false);\n\nfinal usersLogicRef = LogicRef((scope) =\u003e UsersLogic(scope));\n\nclass UsersLogic with Logic implements Loadable {\n  const UsersLogic(this.scope);\n\n  @override\n  final Scope scope;\n\n  UsersRepository get _usersRepository =\u003e use(usersRepositoryRef);\n\n  @override\n  Future\u003cvoid\u003e load() async {\n    write(loadingRef, true);\n    final users = await _usersRepository.fetchAll();\n    write(usersRef, users);\n    write(loadingRef, false);\n  }\n}\n```\n\nFrom the widget side, you'll have to use the `LogicLoader` and provide it the logic references you want to load:\n\n```dart\nclass Home extends StatelessWidget {\n  const Home({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LogicLoader(\n      refs: [usersLogicRef],\n      child: const UsersView(),\n    );\n  }\n}\n```\n\nYou can watch the state in a subtree to display a progress indicator when the data is fetching:\n\n```dart\nclass UsersView extends StatelessWidget {\n  const UsersView({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final loading = context.watch(loadingRef);\n    if (loading) {\n      return const CircularProgressIndicator();\n    }\n\n    // Display the users in a list when have been fetched.\n    final users = context.watch(usersRef);\n    return ListView(...);\n  }\n}\n```\n\nAlternatively, you can use the `builder` parameter to achieve the same goal:\n\n```dart\nclass Home extends StatelessWidget {\n  const Home({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LogicLoader(\n      refs: [usersLogicRef],\n      builder: (context, loading, child) {\n        if (loading) {\n          return const CircularProgressIndicator();\n        }\n\n        // Display the users in a list when have been fetched.\n        final users = context.watch(usersRef);\n        return ListView();\n      },\n    );\n  }\n}\n```\n\n#### Overrides\n\nIt can be useful to be able to override the initial state of `StateRef` or the factory of `LogicRef` in some conditions:\n- When we want a subtree to have its own state/logic under the same reference.\n- For mocking values in tests.\n\n##### Reusing a reference under a different scope.\n\nLet's say we want to create an app where a user can create counters and see the sum of all counters:\n\n![Counters][counters]\n\nWe could do this by having a global state being a list of integers, and a business logic component for adding counters and increment them:\n\n```dart\nfinal countersRef = StateRef(const \u003cint\u003e[]);\n\nfinal countersLogic = LogicRef((scope) =\u003e CountersLogic(scope));\n\nclass CountersLogic with Logic {\n  const CountersLogic(this.scope);\n\n  @override\n  final Scope scope;\n\n  void addCounter() {\n    write(countersRef, read(countersRef).toList()..add(0));\n  }\n\n  void increment(int index) {\n    final counters = read(countersRef).toList();\n    counters[index]++;\n    write(countersRef, counters);\n  }\n}\n```\n\nWe can then use the `select` extension method in a widget to watch the sum of this list:\n\n```dart\nfinal sum = context.watch(countersRef.select(\n  (counters) =\u003e counters.fold\u003cint\u003e(0, (a, b) =\u003e a + b),\n));\n```\n\nNow, for creating the counter view, we can have an `index` parameter in the constructor of this view.\nThis has some drawbacks:\n- 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.\n- We cannot use the `const` keyword anymore.\n\nA 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.\n\nAny `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`.\nThis can be written like this:\n\n```dart\nfinal indexRef = StateRef(0);\n\nclass HomeView extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final countersCount =\n        context.watch(countersRef.select((counters) =\u003e counters.length));\n\n    return Scaffold(\n     ...\n      child: GridView(\n        ...\n        children: [\n          for (int i = 0; i \u003c countersCount; i++)\n            BinderScope(\n              overrides: [indexRef.overrideWith(i)],\n              child: const CounterView(),\n            ),\n        ],\n      ),\n     ...\n    );\n  }\n}\n```\n\nThe `BinderScope` constructor has an `overrides` parameter which can be supplied from an `overrideWith` method on `StateRef` and `LogicRef` instances. \n\n**Note**: The whole code for the above snippets is available in the [example][example_main_overrides] file.\n\n#### Mocking values in tests\n\nLet's say you have an api client in your app:\n\n```dart\nfinal apiClientRef = LogicRef((scope) =\u003e ApiClient());\n```\n\nIf you want to provide a mock instead, while testing, you can do:\n\n```dart\ntestWidgets('Test your view by mocking the api client', (tester) async {\n  final mockApiClient = MockApiClient();\n\n  // Build our app and trigger a frame.\n  await tester.pumpWidget(\n    BinderScope(\n      overrides: [apiClientRef.overrideWith((scope) =\u003e mockApiClient)],\n      child: const MyApp(),\n    ),\n  );\n\n  expect(...);\n});\n```\n\nWhenever the `apiClientRef` is used in your app, the `MockApiClient` instance will be used instead of the real one.\n\n***\n\n### Advanced usage\n\n#### Computed\n\nYou 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.\n**Binder** comes with a `Computed` class to help you with that use case.\n\nLet'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`).\n\nYou could then define the following `Computed` instance:\n\n```dart\nfinal filteredProductsRef = Computed((watch) {\n  final products = watch(productsRef);\n  final minPrice = watch(minPriceRef);\n  final maxPrice = watch(maxPriceRef);\n\n  return products\n      .where((p) =\u003e p.price \u003e= minPrice \u0026\u0026 p.price \u003c= maxPrice)\n      .toList();\n});\n```\n\nLike `StateRef` you can watch a `Computed` in the build method of a widget:\n\n```dart\n@override\nWidget build(BuildContext context) {\n  final filteredProducts = context.watch(filteredProductsRef);\n  ...\n  // Do something with `filteredProducts`.\n}\n```\n\n**Note**: The whole code for the above snippets is available in the [example][example_main_computed] file.\n\n#### Observers\n\nYou may want to observe when the state changed and do some action accordingly (for example, logging state changes).\nTo 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. \n\n```dart\nbool onStateUpdated\u003cT\u003e(StateRef\u003cT\u003e ref, T oldState, T newState, Object action) {\n  logs.add(\n    '[${ref.key.name}#$action] changed from $oldState to $newState',\n  );\n\n  // Indicates whether this observer handled the changes.\n  // If true, then other observers are not called.\n  return true;\n}\n...\nBinderScope(\n  observers: [DelegatingStateObserver(onStateUpdated)],\n  child: const SubTree(),\n);\n```\n\n#### Undo/Redo\n\n**Binder** comes with a built-in way to move in the timeline of the state changes.\nTo be able to undo/redo a state change, you must add a `MementoScope` in your tree.\nThe `MementoScope` will be able to observe all changes made below it:\n\n```dart\nreturn MementoScope(\n  child: Builder(builder: (context) {\n    return MaterialApp(\n      home: const MyHomePage(),\n    );\n  }),\n);\n```\n\nThen, in a business logic, stored below the `MementoScope`, you will be able to call `undo`/`redo` methods.\n\n**Note**: You will get an AssertionError at runtime if you don't provide a `MementoScope` above the business logic calling `undo`/`redo`.\n\n#### Disposable\n\nIn 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.\n\n```dart\nclass MyLogic with Logic implements Disposable {\n  void dispose(){\n    // Do some stuff before this logic go away.\n  }\n}\n```\n\n#### StateListener\n\nIf you want to navigate to another screen or show a dialog when a state change, you can use the `StateListener` widget.\n\nFor example, in an authentication view, you may want to show an alert dialog when the authentication failed.\nTo 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:\n\n```dart\nreturn StateListener(\n  watchable: authenticationResultRef,\n  onStateChanged: (context, AuthenticationResult state) {\n    if (state is AuthenticationFailure) {\n      showDialog\u003cvoid\u003e(\n        context: context,\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('Error'),\n            content: const Text('Authentication failed'),\n            actions: [\n              TextButton(\n                onPressed: () =\u003e Navigator.of(context).pop(),\n                child: const Text('Ok'),\n              ),\n            ],\n          );\n        },\n      );\n    } else {\n      Navigator.of(context).pushReplacementNamed(route_names.home);\n    }\n  },\n  child: child,\n);\n```\n\nIn 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.\n\n#### DartDev Tools\n\n**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`.\n\n***\n\n## Snippets\n\nYou can find code snippets for vscode at [snippets].\n\n## Sponsoring\n\nI'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.\n\n## Contributions\n\nFeel free to contribute to this project.\n\nIf you find a bug or want a feature, but don't know how to fix/implement it, please fill an [issue][issue].  \nIf you fixed a bug or implemented a feature, please send a [pull request][pr].\n\n\u003c!-- Links --\u003e\n[github_action_badge]: https://github.com/letsar/binder/workflows/Build/badge.svg\n[github_action]: https://github.com/letsar/binder/actions\n[pub_badge]: https://img.shields.io/pub/v/binder.svg\n[pub]: https://pub.dartlang.org/packages/binder\n[codecov]: https://codecov.io/gh/letsar/binder\n[codecov_badge]: https://codecov.io/gh/letsar/binder/branch/main/graph/badge.svg\n[binder_logo]: https://raw.githubusercontent.com/letsar/binder/main/images/logo.svg\n[img_data_flow]: https://raw.githubusercontent.com/letsar/binder/main/images/data_flow.png\n[counters]: https://raw.githubusercontent.com/letsar/binder/main/images/counters.gif\n[example_main]: https://github.com/letsar/binder/blob/main/packages/binder/example/lib/main.dart\n[example_main_overrides]: https://github.com/letsar/binder/blob/main/packages/binder/example/lib/main_overrides.dart\n[example_main_computed]: https://github.com/letsar/binder/blob/main/packages/binder/example/lib/main_computed.dart\n[issue]: https://github.com/letsar/binder/issues\n[pr]: https://github.com/letsar/binder/pulls\n[snippets]: https://marketplace.visualstudio.com/items?itemName=romain-rastel.flutter-binder-snippets\n","funding_links":["https://github.com/sponsors/letsar","https://patreon.com/romainrastel","https://www.buymeacoffee.com/romainrastel","paypal.me/RomainRastel"],"categories":["框架","State management [🔝](#readme)"],"sub_categories":["状态管理"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fletsar%2Fbinder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fletsar%2Fbinder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fletsar%2Fbinder/lists"}