{"id":15066439,"url":"https://github.com/nivisi/controllable","last_synced_at":"2026-01-02T22:48:23.465Z","repository":{"id":45169935,"uuid":"487028308","full_name":"nivisi/controllable","owner":"nivisi","description":"🛠 Easy and convenient state management; Set your business logic apart from the UI level.","archived":false,"fork":false,"pushed_at":"2022-11-18T16:46:44.000Z","size":288,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2025-01-22T14:45:23.457Z","etag":null,"topics":["business-logic","dart","dart-package","dartlang","flutter","flutter-package","library","package","state","state-management"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/controllable","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/nivisi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-29T15:48:47.000Z","updated_at":"2022-07-13T05:06:55.000Z","dependencies_parsed_at":"2023-01-23T08:00:50.139Z","dependency_job_id":null,"html_url":"https://github.com/nivisi/controllable","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nivisi%2Fcontrollable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nivisi%2Fcontrollable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nivisi%2Fcontrollable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nivisi%2Fcontrollable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nivisi","download_url":"https://codeload.github.com/nivisi/controllable/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243814893,"owners_count":20352038,"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":["business-logic","dart","dart-package","dartlang","flutter","flutter-package","library","package","state","state-management"],"created_at":"2024-09-25T01:08:05.936Z","updated_at":"2026-01-02T22:48:23.420Z","avatar_url":"https://github.com/nivisi.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# controllable\n\nHome repository for the controllable ecosystem.\n\nEasy and convenient state management. Set your business logic apart from the UI level.\n\n\u003e **Warning**\n\u003e \n\u003e This is an alpha version. The documentation is not finished and will be extended and updated later on.\n\n---\n\n| Package | pub.dev |\n| ------- | ------- |\n| [controllable](https://github.com/nivisi/controllable/tree/develop/packages/controllable) | [![controllable pub version][controllable-pub-version-img]][controllable-pub-version-url] |\n| [controllable_flutter](https://github.com/nivisi/controllable/tree/develop/packages/controllable_flutter) | [![controllable flutter pub version][controllable-flutter-pub-version-img]][controllable-flutter-pub-version-url] |\n| [controllable_generator](https://github.com/nivisi/controllable/tree/develop/packages/controllable_generator) | [![controllable generator pub version][controllable-generator-pub-version-img]][controllable-generator-pub-version-url] |\n\n### Quick overview\n\n#### Emit updates for your state\n\n```dart\n@override\nvoid onUpdateName(String newName) {\n  emitWith(name: newName);\n}\n```\n\n#### Read your state\n\n```dart\nText(context.myController.state.name);\n```\n\n#### Watch for your state changes\n\n```dart\nText(context.myController.state.watch.name);\n```\n\n#### Raise events\n\n```dart\nTextButton(\n  onPressed: () =\u003e context.myController.raiseEvent.updateName('New name'),\n  child: Text('Raise update name event'),\n);\n```\n\n#### And more\n\nFor more details, see below.\n\n---\n\n### Table of contents\n\n- [Example](#example)\n  - [Setup](#setup)\n    - [State](#state)\n    - [Event](#event)\n    - [Now you can declare your controller class](#now-you-can-declare-your-controller-class)\n    - [Run the build runner](#run-the-build-runner)\n  - [Usage](#usage)\n    - [Fill your controller](#fill-your-controller)\n    - [UI](#ui)\n- [How to](#how-to)\n  - [Emit new states](#emit-new-states)\n  - [Watch or read the state](#watch-or-read-the-state)\n  - [Raise events](#raise-events-1)\n  - [Fire side effects](#fire-side-effects)\n  - [Listen for side effects](#listen-for-side-effects)\n- [Best practices](#best-practices)\n  - [Use the generated interface](#use-the-generated-interface)\n- [How it all works?](#how-it-all-works)\n  - [Yes, code generation. But let me explain](#yes-code-generation-but-let-me-explain)\n\n## Example\n\n### Setup\n\nAdd packages to __pubspec.yaml__:\n\n```\ndependencies:\n  controllable_flutter:\n  \ndev_dependencies:\n  build_runner:\n  controllable_generator:\n```\n\nAt first, declare your `State` and `Event` classes. For simplicity we'll use an `int` as a side effect, but in fact it can be any class or even an enum.\n\n#### State\n\nPut the required data fields of your state as getters. These will be accessible on the UI and the controller level.\n\n```dart\npart of 'home_controller.dart';\n\nabstract class HomeState extends XState {\n  String get name;\n  String? get address;\n}\n```\n\nDescribe what events can the UI trigger as methods. These will be used by the UI to trigger controller actions.\n\n#### Event\n\n```dart\npart of 'home_controller.dart';\n\nabstract class HomeEvent extends XEvent {\n  void updateName(String newName);\n  void updateAddress(String newAddress);\n  void updateCounter(int counter);\n}\n```\n\n#### Now you can declare your controller class\n```dart\n/* imports */\n\npart 'home_controller.x.dart';\npart 'home_event.dart';\npart 'home_state.dart';\n\n@XControllable\u003cHomeEvent\u003e()\nclass HomeController extends XController\u003cHomeState\u003e with _$HomeController {\n  @override\n  HomeState createInitialState() {\n    // We will fill it later on.\n  }\n}\n\n```\n\n#### Run the build runner\n\n```\nflutter pub run build_runner build\n```\n\n### Usage\n\n#### Fill your controller\n\nA file with settings and extensions for your controller was generated. You can now fill details of your controller:\n\n```dart\n@XControllable\u003cHomeEvent\u003e()\nclass HomeController extends XController\u003cHomeState\u003e with _$HomeController {\n  @override\n  HomeState createInitialState() {\n    // Use the method below to create the initial state of your controller.\n    // The parameters are the same as you declared in the HomeState class.\n    return createHomeState(name: 'something');\n  }\n\n  // Methods below are called events. These will be raised from the UI level.\n  // They are generated based on the HomeEvent class.\n  // Notice, the updateName method became onUpdateName and so on.\n\n  @override\n  void onUpdateAddress(String newAddress) {\n    // Use emitWith to deliver a new state with updated fields to your UI.\n    emitWith(address: newAddress);\n  }\n\n  @override\n  void onUpdateName(String newName) {\n    emitWith(name: newName);\n  }\n\n  @override\n  void onUpdateCounter(int counter) {\n    // Use fireEffect to fire an effect that the UI layer can catch\n    // Using XListener widget.\n    // It then can navigate to another page, show a toast etc\n    fireEffect(counter);\n  }\n}\n```\n\n#### UI\n\nProvide `HomeController`:\n\n```dart\nreturn XProvider(\n  create: (context) =\u003e HomeController(),\n  child: const HomeBody(),\n);\n```\n\nIn `HomeBody`, access the fields:\n\n```dart\nfinal controller = context.homeController;\n\nreturn Column(\n  children: [\n    Text(controller.state.watch.name),\n    Text(controller.state.watch.address),\n    TextButton(\n      // The onUpdateName method of the controller will be called here.\n      onPressed: () =\u003e controller.raiseEvent.updateName('New Name'),\n      child: Text('Set name to \"New Name\"'),\n    ),\n  ],\n);\n```\n\nThe `watch` statement will make the widget of the given `BuildContext` to rebuild whenever the corresponding field changes. So, in the example above, whenever you do either `emitWith(name: any);` or `emitWith(address: any);`, the tree will get rebuilt. But we want to avoid unnecesseary rebuilds, right? Move the texts to separate widgets then! Or wrap them with builders:\n\n```dart\nreturn Column(\n  children: [\n    Builder(\n      // This will be rebuilt only when name changes \n      // Because this `context` now is only related to this part of the tree!\n      builder: (context) =\u003e Text(context.homeController.state.watch.name)\n    ),\n    Builder(\n      // This will be rebuilt only when address changes\n      // Because this `context` now is only related to this part of the tree!\n      builder: (context) =\u003e Text(context.homeController.state.watch.address)\n    ),\n    // This button will not get rebuilt when neither name or address changes\n    TextButton(\n      onPressed: () =\u003e context.homeController.raiseEvent.updateName('New Name'),\n      child: Text('Set name to \"New Name\"'),\n    ),\n  ],\n);\n```\n\nIf you want to simply read a field w/o watching for it, just access it w/o watch:\n\n```dart\nonPressed: () =\u003e print(context.homeController.state.name),\n```\n\n## How to\n\n### Emit new states\n\nIn your controller, use the `emitWith` to deliver a new state to your UI. Call `emitWith(yourField: newValue)` to update `yourField`.\n\n### Watch or read the state\n\nUse `context.controller.state.watch.*` for listening for state field updates.\n\nUse `context.controller.state.*` for simply reading the state fields.\n\n### Raise events\n\nTo make the controller to perform certain business logic, on the UI level do `context.yourController.raiseEvent.yourEvent`. This will execute the corresponding method in your controller.\n\n### Fire side effects\n\nSide effects are needed to perform UI actions. Navigation to another screen, showing a dialog or a toast, validating a form field ect. — this is what side effects are for.\n\nIn your controller, do:\n\n```dart\n  fireEffect(data);\n```\n\n### Listen for side effects\n\nUse the `XListener` widget to listen for it side effects that are fired by your controllers.\n\n```dart\nXListener(\n  streamable: context.homeController,\n  listener: (context, effect) {\n    // Do whatever is required on the UI level.\n    print(effect); // or just print the effect...\n\n    // To check for certain types of effects, you can do:\n    if (effect is MyEffect) {\n      // Do sth with MyEffect\n    }\n  },\n  child: const SomeWidget(),\n);\n```\n\n## Best practices\n\nThey are yet to determine! :) But the first one is:\n\n### Use the generated interface\n\nAccess the state and the events only via the `context.controller.state` / `context.controller.raiseEvent`. Still, you can get your controller via the `Provider`'s `context.read\u003cYourController\u003e()` or other methods. But it is recommended to use only the `BuildContext` extensions for that.\n\nOn your UI level the interface of your controller has only three fields: `state`, `raiseEvent` and `effectStream`. Though the latter is only for `XListener` widgets. So use:\n- `state` for reading/watching values and rendering the UI;\n- `raiseEvent` for triggering actions in the controller.\n\n## How it all works?\n\nIt is all possible with the power of mixins and extensions.\n\n_TODO: Describe it._\n\n### Yes, code generation. But let me explain\n\nControllable generates code uniquely for your controllers so you can avoid writing boilerplate code. Also, it creates an interface for public methods that the UI should use and state fields that the UI should render.\n\n\u003c!--References--\u003e\n[controllable-pub-version-img]: https://img.shields.io/badge/pub-v0.0.5-0175c2?logo=flutter\n[controllable-pub-version-url]: https://pub.dev/packages/controllable\n\n[controllable-flutter-pub-version-img]: https://img.shields.io/badge/pub-v0.0.5+1-0175c2?logo=flutter\n[controllable-flutter-pub-version-url]: https://pub.dev/packages/controllable_flutter\n\n[controllable-generator-pub-version-img]: https://img.shields.io/badge/pub-v0.0.8-0175c2?logo=flutter\n[controllable-generator-pub-version-url]: https://pub.dev/packages/controllable_generator\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnivisi%2Fcontrollable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnivisi%2Fcontrollable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnivisi%2Fcontrollable/lists"}