{"id":15476786,"url":"https://github.com/brianegan/flutter_stream_friends","last_synced_at":"2025-04-22T14:22:14.991Z","repository":{"id":56830295,"uuid":"112040597","full_name":"brianegan/flutter_stream_friends","owner":"brianegan","description":"Flutter's great. Streams are great. Let's be friends.","archived":false,"fork":false,"pushed_at":"2018-05-27T16:02:08.000Z","size":156,"stargazers_count":62,"open_issues_count":2,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-29T15:51:08.790Z","etag":null,"topics":["dart-streams","flutter","rx"],"latest_commit_sha":null,"homepage":null,"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/brianegan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2017-11-25T23:04:21.000Z","updated_at":"2023-08-15T06:49:40.000Z","dependencies_parsed_at":"2022-09-02T05:51:01.528Z","dependency_job_id":null,"html_url":"https://github.com/brianegan/flutter_stream_friends","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fflutter_stream_friends","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fflutter_stream_friends/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fflutter_stream_friends/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fflutter_stream_friends/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brianegan","download_url":"https://codeload.github.com/brianegan/flutter_stream_friends/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250255917,"owners_count":21400434,"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":["dart-streams","flutter","rx"],"created_at":"2024-10-02T03:41:07.994Z","updated_at":"2025-04-22T14:22:14.948Z","avatar_url":"https://github.com/brianegan.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# flutter_stream_friends\n\n[![Build Status](https://travis-ci.org/brianegan/flutter_stream_friends.svg?branch=master)](https://travis-ci.org/brianegan/flutter_stream_friends)  [![codecov](https://codecov.io/gh/brianegan/flutter_stream_friends/branch/master/graph/badge.svg)](https://codecov.io/gh/brianegan/flutter_stream_friends)\n\nConnect Flutter Widgets to Dart Streams! In Flutter, there's a wonderful distinction between `StatefulWidgets` and `StatelessWidgets`. When used well, `StatefulWidgets` provide a convenient way to encapsulate your data coordination needs in one component, and keep the UI rendering in various \"passive\" `StatelessWidgets`. In React terms, this is often called the \"Smart Component / Dumb Component\" pattern, and is similar to the \"Active Presenter / Passive View\" pattern in MVP.\n\nHowever, what if you've got slightly more advanced data needs, such as loading\ndata from a database or web server? Furthermore, you may need to listen to a continuous stream of updates from a Store or EventBus. Finally, you may require more powerful control over your event-handling, such as being able to `debounce` or `buffer` the events passing through an event-handler. For these use cases, Streams provide a great way to manage the events and data needs of a `StatefulWidget`!\n\nIn general: what if we could combine the power of `StatefulWidgets` with the elegance of `Streams`? That's just what this library aims to help with.\n\n## How it works\n\nIn order to understand the concept, let's compare the default usage of `StatefulWidget` to a `StreamBuilder` version. This library used to provide a `StreamWidget`, but we now recommend using the new `StreamBuilder` widget provided by the Flutter framework. \n\n### Original\n \nLet's start with the simple counter example that comes out of the box when you create a new Flutter app. The important parts are:\n\n  - Create a `StatefulWidget` with a corresponding `State` object\n  - Within the `State` object, create widget state and event handlers\n  - The event handlers are responsible for updating the local state of the widget\n  - Use these pieces of state within the `build` method. \n\n```dart\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key key, this.title}) : super(key: key);\n\n  // This widget is the home page of your application. It is stateful,\n  // meaning that it has a State object (defined below) that contains\n  // fields that affect how it looks.\n\n  // This class is the configuration for the state. It holds the\n  // values (in this case the title) provided by the parent (in this\n  // case the App widget) and used by the build method of the State.\n  // Fields in a Widget subclass are always marked \"final\".\n\n  final String title;\n\n  @override\n  _MyHomePageState createState() =\u003e new _MyHomePageState();\n}\n\nclass _MyHomePageState extends State\u003cMyHomePage\u003e {\n  int _counter = 0;\n\n  void _incrementCounter() {\n    setState(() {\n      // This call to setState tells the Flutter framework that\n      // something has changed in this State, which causes it to rerun\n      // the build method below so that the display can reflect the\n      // updated values. If we changed _counter without calling\n      // setState(), then the build method would not be called again,\n      // and so nothing would appear to happen.\n      _counter++;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // This method is rerun every time setState is called, for instance\n    // as done by the _incrementCounter method above.\n    // The Flutter framework has been optimized to make rerunning\n    // build methods fast, so that you can just rebuild anything that\n    // needs updating rather than having to individually change\n    // instances of widgets.\n    return new Scaffold(\n      appBar: new AppBar(\n        // Here we take the value from the MyHomePage object that\n        // was created by the App.build method, and use it to set\n        // our appbar title.\n        title: new Text(config.title),\n      ),\n      body: new Center(\n        child: new Text(\n          'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.',\n        ),\n      ),\n      floatingActionButton: new FloatingActionButton(\n        onPressed: _incrementCounter,\n        tooltip: 'Increment',\n        child: new Icon(Icons.add),\n      ), // This trailing comma tells the Dart formatter to use\n      // a style that looks nicer for build methods.\n    );\n  }\n}\n```\n\n### StreamBuilder version\n\nNow, let's take a look at the version using streams! This code will produce the exact same UI, but the way it manages state is a bit different. Rather than relying on local state within a `State` object, using handlers to `setState`, we use the power of the Dart `Stream` to continually listen to and deliver new information to the Widget in response to button presses!\n\nHow it works:\n\n  - Create a Stateless widget that contains a `StreamBuilder`\n  - The `StreamBuilder` takes a `stream` parameter. Instead of creating a `State` object to manage the counter state, we'll create a `Stream` instead that will deliver the current count.\n  - The `Stream` we build contains a `VoidStreamCallback` that acts as both the `onPressed` handler on the `floatingActionButton` and as the stream we'll listen to so we know when the button is pressed.\n  - Then, as the button is pressed, the Stream will deliver the latest value to the \n\nNow that we've chatted a bit about how it works, let's see the code!\n\n```dart\nclass MyApp extends StatelessWidget {\n  static String appTitle = \"Flutter Stream Friends\";\n\n  @override\n  Widget build(BuildContext context) {\n    return new MaterialApp(\n      title: appTitle,\n      theme: new ThemeData(\n        primarySwatch: Colors.purple,\n      ),\n      home: new StreamBuilder(\n          stream: new CounterScreenStream(appTitle),\n          builder: (context, snapshot) =\u003e buildHome(\n              context,\n              snapshot.hasData\n                  // If our stream has delivered data, build our Widget properly\n                  ? snapshot.data\n                  // If not, we pass through a dummy model to kick things off\n                  : new CounterScreenModel(0, () {}, appTitle))),\n    );\n  }\n\n  // The latest value of the CounterScreenModel from the CounterScreenStream is\n  // passed into the this version of the build function!\n  Widget buildHome(BuildContext context, CounterScreenModel model) {\n    return new Scaffold(\n      appBar: new AppBar(\n        title: new Text(model.title),\n      ),\n      body: new Center(\n        child: new Text(\n          'Button tapped ${ model.count } time${ model.count == 1\n              ? ''\n              : 's' }.',\n        ),\n      ),\n      floatingActionButton: new FloatingActionButton(\n        // Use the `StreamCallback` here to wire up the events to the Stream.\n        onPressed: model.onFabPressed,\n        tooltip: 'Increment',\n        child: new Icon(Icons.add),\n      ),\n    );\n  }\n}\n\nclass CounterScreenStream extends Stream\u003cCounterScreenModel\u003e {\n  final Stream\u003cCounterScreenModel\u003e _stream;\n\n  CounterScreenStream(String title,\n      [VoidStreamCallback onFabPressed, int initialValue = 0])\n      : this._stream = createStream(\n            title, onFabPressed ?? new VoidStreamCallback(), initialValue);\n\n  @override\n  StreamSubscription\u003cCounterScreenModel\u003e listen(\n          void onData(CounterScreenModel event),\n          {Function onError,\n          void onDone(),\n          bool cancelOnError}) =\u003e\n      _stream.listen(onData,\n          onError: onError, onDone: onDone, cancelOnError: cancelOnError);\n\n  // The method we use to create the stream that will continually deliver data\n  // to the `buildHome` method.\n  static Stream\u003cCounterScreenModel\u003e createStream(\n      String title, VoidStreamCallback onFabPressed, int initialValue) {\n    return new Observable(onFabPressed) // Every time the FAB is clicked\n        .map((_) =\u003e 1) // Emit the value of 1\n        .scan(\n            (int a, int b, int i) =\u003e a + b, // Add that 1 to the total\n            initialValue)\n        // Before the button is clicked, kick everything off by emitting 0\n        .startWith(initialValue)\n        // Convert the latest count and the event handler into the Widget Model\n        .map((int count) =\u003e new CounterScreenModel(count, onFabPressed, title));\n  }\n}\n\nclass CounterScreenModel {\n  final String title;\n  final int count;\n  final VoidCallback onFabPressed;\n\n  CounterScreenModel(this.count, this.onFabPressed, this.title);\n\n  // If you've got a custom data model for your widget, it's best to implement\n  // the == method in order to take advantage the performance optimizations\n  // offered by the `Streams#distinct()` method. This will ensure the Widget is\n  // repainted only when the Model has truly changed.\n  @override\n  bool operator ==(Object other) =\u003e\n      identical(this, other) ||\n      other is CounterScreenModel \u0026\u0026\n          runtimeType == other.runtimeType \u0026\u0026\n          title == other.title \u0026\u0026\n          count == other.count \u0026\u0026\n          onFabPressed == other.onFabPressed;\n\n  @override\n  int get hashCode =\u003e title.hashCode ^ count.hashCode ^ onFabPressed.hashCode;\n\n  @override\n  String toString() =\u003e\n      'CounterScreenModel{title: $title, count: $count, onFabPressed: $onFabPressed}';\n}\n```\n\n## Why would you do this madness!?\n\nYou might ask: Why would you do this? The second version is so much more code! And you're right, for a super simple example, such as a counter, this is indeed much more code.\n\nHowever, there are some important advantages: First, separation of concerns. The state logic is now properly encapsulated as a Stream is easily testable. This should not be undervalued.\n\nSecond, it makes your state management fundamentally reactive! That means your Widgets can stay up to date with a variety\nof data sources that emit state changes (think Firebase or WebSockets or Redux). For example:\n\n  - You may have more complex data needs, such as:\n    - calling a local database, file system, or web service when your Widget initializes\n    - Keeping your Widgets up to date with a reactive data source, such as a Firebase Database, WebSocket, or Redux Store \n  - No longer make manual calls to `setState`. Just set up your stream and the `StreamWidget` handles the rest.\n  - You can use the power of Streams to reduce the number redraws your UI performs. By using `Stream#distinct` under the hood, setState will only be called when data is truly fresh.\n  - No need to worry about manually canceling any StreamSubscriptions.  \n  - Helpful when you have more advanced event handling needs, such as needing to `debounce` or `buffer` the events.\n  \n## Examples\n\nYou can check out the `example` directory showing the code above implemented as a real Flutter app.\n\nAnother project is being worked on that also demonstrates this concept when listening to a Redux store!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrianegan%2Fflutter_stream_friends","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrianegan%2Fflutter_stream_friends","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrianegan%2Fflutter_stream_friends/lists"}