{"id":21748382,"url":"https://github.com/surfstudio/flutter-relation","last_synced_at":"2026-03-09T20:03:56.652Z","repository":{"id":46413712,"uuid":"384333277","full_name":"surfstudio/flutter-relation","owner":"surfstudio","description":null,"archived":false,"fork":false,"pushed_at":"2023-02-27T06:59:14.000Z","size":1374,"stargazers_count":7,"open_issues_count":4,"forks_count":1,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-13T07:15:42.354Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/surfstudio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-07-09T05:40:27.000Z","updated_at":"2023-05-30T08:23:55.000Z","dependencies_parsed_at":"2025-04-13T07:15:46.225Z","dependency_job_id":"40c11ae7-3a38-476e-99ea-ee5fad58938e","html_url":"https://github.com/surfstudio/flutter-relation","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/surfstudio/flutter-relation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/surfstudio%2Fflutter-relation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/surfstudio%2Fflutter-relation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/surfstudio%2Fflutter-relation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/surfstudio%2Fflutter-relation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/surfstudio","download_url":"https://codeload.github.com/surfstudio/flutter-relation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/surfstudio%2Fflutter-relation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30310011,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T17:35:44.120Z","status":"ssl_error","status_checked_at":"2026-03-09T17:35:43.707Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-11-26T08:13:19.098Z","updated_at":"2026-03-09T20:03:56.616Z","avatar_url":"https://github.com/surfstudio.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Relation\n\n[![Build Status](https://github.com/surfstudio/SurfGear/workflows/build/badge.svg)](https://github.com/surfstudio/SurfGear)\n[![Coverage Status](https://codecov.io/gh/surfstudio/SurfGear/branch/dev/graph/badge.svg?flag=relation)](https://codecov.io/gh/surfstudio/SurfGear)\n[![Pub Version](https://img.shields.io/pub/v/relation)](https://pub.dev/packages/relation)\n[![Pub Likes](https://badgen.net/pub/likes/relation)](https://pub.dev/packages/relation)\n![Flutter Platform](https://badgen.net/pub/flutter-platform/relation)\n\nThis package is part of the [SurfGear](https://github.com/surfstudio/SurfGear) toolkit made by [Surf](https://surf.ru/).\n\n![Relation Cover](https://i.ibb.co/f1yC8d5/relation-logo.png)\n\n## Description\n\nTwo-way communication channels for transferring data between different architectural layers of a Flutter application.\n\n## Currently supported features\n\n- Notify your app's presentation layer about every user input or UI event (button tap, focus change, gesture detection, etc.) using `StreamedAction` and implement a reaction to them;\n- Write less code with *StreamedAction* that are customized for specific user cases (scrolling, editing text, `ValueNotifier` value changing).\n- React to the data state changes and redraw UI using `StreamedState` together with `StreamedStateBuilder` and its variations;\n- Manage the screen state easily with a special stream that handles three predefined states: data, loading, error.\n\n## Example\n\n### Notify and react\n\n#### StreamedAction\n\n![StreamedAction Scheme](https://i.ibb.co/rcvRN44/Streamed-Action-scheme.png)\n\n`StreamedAction` is a good way to notify consumers about every event coming from the UI.\n\nCreate an `StreamedAction` class instance. You can pass data with `StreamedAction`'s events, so you need to specify the concrete type of `StreamedAction` while declaring it.\n\n```dart\nfinal logoutAction = VoidAction();\n\nfinal addItemToCartAction = StreamedAction\u003cItem\u003e();\n```\n\nFind the place where you're going to handle events triggered by your `StreamedAction`. Subscribe to the event stream. You can access it through the `stream` property.\n\n```dart\nlogoutAction.stream.listen(\n  (_) =\u003e logout()\n);\n\naddItemToCartAction.stream.listen(\n  (item) =\u003e addItemToCart(item)\n);\n```\n\nNow you can trigger an event through an `StreamedAction` instance from anywhere just like that:\n\n```dart\nlogoutAction.accept();\n\naddItemToCartAction.accept(item);\n```\n\nOr even easier:\n\n```dart\nTextButton(\n  onPressed: logoutAction,\n  ...\n),\n```\n\n#### StreamedState\n\n![StreamedState Scheme](https://i.ibb.co/nwZWsP2/relation-streamed-state.png)\n\nWith `StreamedState` you can notify consumers of data changes.\n\nCreate a `StreamedState` class instance. `StreamedState` constructor allows you to set the initial value that the consumers will receive as soon as they subscribe to the `StreamedState`. You need to specify the data type that your `StreamedState` will handle.\n\n```dart\nfinal userBalanceState = StreamedState\u003cint\u003e(0);\n\nfinal itemsInCartState = StreamedState\u003cList\u003cItem\u003e\u003e();\n```\n\nYou can subscribe to `StreamedState` changes the same way as with `Action`.\n\n```dart\nuserBalanceState.stream.listen(\n  (balance) =\u003e showUserBalance(balance)\n);\n```\n\nTo notify consumers of any data changes, you can release the relevant data to the `StreamedState` via the `accept()` function.\n\n```dart\nuserBalanceState.accept(100);\n```\n\nIn fact, you can use `Action`s and `StreamedState`s to communicate between any objects in your application. However, we recommend using them to connect the UI and presentation layers.\n\n### Update UI\n\n#### StreamStateBuilder\n\n![StreamedStateBuilder Scheme](https://i.ibb.co/xhVBkt8/relation-streamed-state-builder.png)\n\n`StreamStateBuilder` is a widget built on the latest snapshot of interaction with a `StreamedState`. The `StreamStateBuilder`'s behavior is almost the same as the standard `StreamBuilder`. The only difference is that it accepts `StreamedState` instead of the usual `Stream`, thus simplifying the initial data setup.\n\n`StreamStateBuilder` rebuilds its widget subtree each time as its corresponding `StreamedState` emits a new value. This is the recommended way to organize your UI layer. It can save you from multiple `setState()` function calls.\n\n```dart\nContainer(\n  child: StreamedStateBuilder(\n    streamedState: userBalanceState,\n    builder: (ctx, balance) =\u003e _buildUserBalanceWidget(balance),\n  ),\n)\n```\n\n### State Management\n\n![State Management Scheme](https://i.ibb.co/YcnGww0/relation-state-management.png)\n\nYou can build a state management solution for your Flutter app using all of the components above.\n\nWe recommend using **Relation** package in conjunction with [MWWM architecture](https://pub.dev/packages/mwwm).\n\n- Use `StreamedAction` to notify the presentation layer of all UI events (button taps, pull-to-refresh triggers, swipes, or other gestures detections);\n- Use `StreamedState` to report any data changes to the UI layer;\n- Let `StreamedStateBuilder` manage the UI state for you. It will rebuild all its child widgets right after it detects any newly released data in the associated `StreamedState`.\n\n## Extra units\n\nThe **Relation** package provides you not only with some basic components for common use cases, but with even more highly specialized classes for solving specific issues.\n\n### Extra StreamedActions\n\n#### ScrollOffsetActon\n\nYou can use special `ScrollOffsetAction` to track the scroll offset of a scrollable widget. This is possible thanks to the built-in `ScrollController`.\n\n```dart\nfinal scrollOffsetAction = ScrollOffsetAction();\n\nscrollOffsetAction.stream.listen((offset) {\n  print(\"Current scroll offset = $offset\");\n});\n\nSingleChildScrollView(\n  controller: scrollOffsetAction.controller,\n)\n```\n\n#### TextEditingActon\n\n`TextEditingAction` is a special type of **Action** that tracks text changes in the text field. The built-in `TextEditingController` makes it possible.\n\n```dart\nfinal textEditingAction = TextEditingAction();\n\ntextEditingAction.stream.listen((text) {\n    print(\"Typed text = $text\");\n});\n\nTextField(\n    controller: textEditingAction.controller,\n    onChanged: textEditingAction,\n),\n```\n\n#### ControllerActon\n\n`ControllerAction` is more common than the two previous variations. You can pass a [`ValueNotifier`](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) inheritor during the `ControllerAction` instantiation.\n\nThis means you can work with the `ClipboardStatusNotifier`, `TextEditingController` or `TransformationController` through the `ControllerAction`.\n\n### Extra StreamedStates\n\n#### EntityStreamedState + EntityStateBuilder\n\n`EntityStreamedState` is an extended version of `StreamedState` designed to make implementing typical dynamic data screens easier.\n\nMost screens in mobile applications are quite simple and usually have several typical states:\n\n- data;\n- loading;\n- error.\n\n`EntityStreamedState` provides you with a convenient interface for the data stream to handle these states properly.\n\nCreate a `EntityStreamedState` class instance. It has the same abilities as `StreamedState`: the initial value setup and the specific data type declaration. Keep in mind that `EntityStreamedState` accepts an `EntityState` wrapper around your data rather than a raw part of your data.\n\n```dart\nfinal userProfileState = EntityStreamedState\u003cUserProfile\u003e(EntityState(isLoading: true));\n```\n\nNow you can switch your `EntityStreamedState`'s state with just a simple function call. A typical workflow for a query providing some data would look like this:\n\n```dart\nuserProfileState.loading();\ntry {\n  final result = await _loadUserProfile();\n  userProfileState.content(result);\n} on Exception catch (error) {\n  userProfileState.error(error);\n}\n```\n\nBut what do all these functions actually do? The answer is on the other side. By using `EntityStateBuilder` instead of just `StreamedStateBuilder` you can set widgets for all three states and switch between them easily.\n\nPass `EntityStreamedState` instance to the `streamedState` argument first. After that, you can specify a set of widgets for displaying data (`child`), load state (`loadingChild`), and error state (`errorChild`).\n\n```dart\nEntityStateBuilder\u003cUserProfile\u003e(\n  streamedState: userProfileState,\n  builder: (ctx, data) =\u003e UserProfileWidget(data),\n  loadingChild: CircularProgressIndicator(),\n  errorChild: ErrorWidget('Something went wrong. Please, try again'),\n),\n```\n\nAnother way to deal with `EntityStateBuilder` is to use `loadingBuilder` and `errorBuilder`. This allows you to customize the error state widgets because you can account for the type of error and the last registered data value from the data stream received by the `errorBuilder`. The same with `loadingBuilder`.\n\n```dart\nEntityStateBuilder\u003cUserProfile\u003e(\n  streamedState: userProfileState,\n  builder: (ctx, data) =\u003e UserProfileWidget(data),\n  loadingBuilder: (context, data) {\n    return LoadingWidget(data);\n  },\n  errorBuilder: (context, data, error) {\n    return ErrorWidget(error);\n  },\n),\n```\n\nTo summarize, every time someone calls an `EntityStateBuilder`'s functions (`loading()`, `content()` or `error()`), the builder redraws its widget subtree and displays the state that corresponds to the last call.\n\n#### TextFieldStreamedState + TextFieldStateBuilder\n\nThe idea behind `TextFieldStreamedState` and `TextFieldStateBuilder` is technically the same. The only difference is that `TextFieldStreamedState` is designed to work with text widgets (`Text`, `TextField`, etc.).\n\n`TextFieldStreamedState` allows you to set up your text field validation rules and some other settings, such as making the text field mandatory for the user to fill out.\n\n```dart\nfinal textState = TextFieldStreamedState(\n  'initialString',\n  validator: '[a-zA-Z]{3,30}',\n  canEdit: true,\n  incorrectTextMsg: 'Text is invalid',\n  mandatory: true,\n);\n```\n\n`TextFieldStateBuilder` accepts the `TextFieldStreamedState` instance and allows to create text widgets according to all state properties.\n\n```dart\nTextFieldStateBuilder(\n  state: textState,\n  stateBuilder: (context, textStateValue) {\n    return Text(textStateValue.data);\n  },\n),\n```\n\n## Installation\n\nAdd `relation` to your `pubspec.yaml` file:\n\n```yaml\ndependencies:\n  relation: ^2.0.0\n```\n\nYou can use both `stable` and `dev` versions of the package listed above in the badges bar.\n\n## Changelog\n\nAll notable changes to this project will be documented in [this file](./CHANGELOG.md).\n\n## Issues\n\nFor issues, file directly in the Issues section.\n\n## Contribute\n\nIf you would like to contribute to the package (e.g. by improving the documentation, solving a bug or adding a cool new feature), please review our [contribution guide](../../CONTRIBUTING.md) first and send us your pull request.\n\nYour PRs are always welcome.\n\n## How to reach us\n\nPlease feel free to ask any questions about this package. Join our community chat on Telegram. We speak English and Russian.\n\n[![Telegram](https://img.shields.io/badge/chat-on%20Telegram-blue.svg)](https://t.me/SurfGear)\n\n## License\n\n[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsurfstudio%2Fflutter-relation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsurfstudio%2Fflutter-relation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsurfstudio%2Fflutter-relation/lists"}