{"id":22449877,"url":"https://github.com/turskyi/flutter_clean_architecture_counter","last_synced_at":"2026-04-17T07:31:30.102Z","repository":{"id":252156650,"uuid":"839597474","full_name":"Turskyi/flutter_clean_architecture_counter","owner":"Turskyi","description":"A refactored version of the default Flutter \"counter\" app, demonstrating The Clean Architecture with reactive state management using streams. This example showcases a maintainable and scalable approach, incorporating immutability, dependency injection, and a clear separation of concerns.","archived":false,"fork":false,"pushed_at":"2025-04-27T23:33:09.000Z","size":1224,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-28T00:27:52.799Z","etag":null,"topics":["architecture-patterns","clean-architecture","clean-code","counter-app","dart","dependency-injection","example-project","flutter","flutter-examples","reactive-programming","software-architecture","state-management","stream","streams"],"latest_commit_sha":null,"homepage":"https://dartpad.dev/?id=e4faa4d8c6fd26106d566af076795917","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Turskyi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2024-08-08T00:12:12.000Z","updated_at":"2025-04-27T23:33:12.000Z","dependencies_parsed_at":"2024-08-08T02:45:08.241Z","dependency_job_id":"90ae0a64-eb8d-4dc0-b83f-1c733c0fa6fe","html_url":"https://github.com/Turskyi/flutter_clean_architecture_counter","commit_stats":null,"previous_names":["turskyi/flutter_clean_architecture_counter"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Turskyi/flutter_clean_architecture_counter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fflutter_clean_architecture_counter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fflutter_clean_architecture_counter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fflutter_clean_architecture_counter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fflutter_clean_architecture_counter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Turskyi","download_url":"https://codeload.github.com/Turskyi/flutter_clean_architecture_counter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Turskyi%2Fflutter_clean_architecture_counter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31919873,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"online","status_checked_at":"2026-04-17T02:00:06.879Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["architecture-patterns","clean-architecture","clean-code","counter-app","dart","dependency-injection","example-project","flutter","flutter-examples","reactive-programming","software-architecture","state-management","stream","streams"],"created_at":"2024-12-06T05:12:20.022Z","updated_at":"2026-04-17T07:31:30.072Z","avatar_url":"https://github.com/Turskyi.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)\n[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints)\n[![Code Quality](https://github.com/Turskyi/flutter_clean_architecture_counter/actions/workflows/code-quality-tests.yml/badge.svg?branch=master\u0026event=push)](https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md)\n[![codecov](https://codecov.io/gh/Turskyi/flutter_clean_architecture_counter/graph/badge.svg?token=R2N9ND3EQD)](https://codecov.io/gh/Turskyi/flutter_clean_architecture_counter)\n\u003cimg alt=\"GitHub commit activity\" src=\"https://img.shields.io/github/commit-activity/m/Turskyi/flutter_clean_architecture_counter\"\u003e\n\n# Flutter Clean Architecture Counter With Stream\n\nThis project is a refactored version of the\n[default Flutter counter app](https://dartpad.dev/?sample=counter),\ndemonstrating how it could be implemented in a production environment using\nClean Architecture, originally introduced by\n[Robert Cecil Martin](https://en.wikipedia.org/wiki/Robert_C._Martin) in his \narticles \n[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2011/11/22/Clean-Architecture.html) \nand\n[The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).\n\nThe default counter app is often criticized for its simplicity and lack of\nreal-world applicability. This example showcases a more maintainable and\nscalable approach, incorporating immutability, dependency injection, and\nreactive state management using \n[streams](https://dart.dev/libraries/async/using-streams).\n\n\u003c!--suppress CheckImageSize --\u003e\n\u003ca href=\"https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html\"\u003e\n\u003c!--suppress CheckImageSize --\u003e\n\u003cimg src=\"documentation/CleanArchitecture.jpg\" width=\"800\" title=\"Onion Architecture\" alt=\"Image of the Clean Architecture Pattern\"\u003e\n\u003c/a\u003e\n\n## Architecture Overview\n\nThe project follows the four main layers of Clean Architecture:\n\n1. **Enterprise Business Rules**: Core business logic and entities.\n2. **Application Business Rules**: Business rules and operations.\n3. **Interface Adapters**: Application-specific logic and orchestration, \nincluding the User Interface.\n4. **Frameworks \u0026 Drivers**: Technical details like the database and web \nframeworks, and the main entry point of the application.\n\n### Enterprise Business Rules (Entities) Layer\n\nContains the `Counter` entity. This layer does not depend on anything else, \nwhich is evident from the imports in the class.\n\n### Application Business Rules (Use Cases) Layer\n\nContains the `IncrementCounter` interface and its implementation \n`IncrementCounterFakeImpl`. This layer depends only on the Entities layer. \nIt also includes the `CounterGateway` interface to allow use cases to \ncommunicate with the outer layers.\n\n### Interface Adapters Layer\n\nContains the `CounterPresenter` which manages the state and business logic. \nThis layer depends on both Use Cases and Entities layers. \nIt also includes the User Interface components like the `MyHomePage` widget \nand the implementation of the Gateway (`FakeCounterGateway`).\n\n### Frameworks \u0026 Drivers Layer\n\nContains technical details like the database and web frameworks. This layer \nalso includes the main entry point of the application, where dependencies are \ninjected and the application is initialized.\n\nThe Frameworks \u0026 Drivers components have access to all inner layers.\n\n### Note on Layer Separation\n\nFor the sake of simplicity, the inner layers are not decoupled into separate \npackages in this example. \nIn a production environment, it is essential to enforce the dependency flow by \nseparating these layers into different packages. \nThis ensures that, for example, the Entities layer cannot access the \nFrameworks \u0026 Drivers Layer.\n\n### Project Structure\n\nThe simplified structure of the project is as follows:\n\n```\nlib/\n├── main.dart\n├── interface_adapters/\n├── use_cases/\n└── entities/\n```\n\n## Getting Started\n\nTo get started with this project, clone the repository and run the following\ncommands:\n\n```bash\nflutter pub get\nflutter run\n```\n\nYou can also test the implementation directly on DartPad:\nhttps://dartpad.dev/?id=e4faa4d8c6fd26106d566af076795917\n\n## Running Tests\n\nTo run the tests, use the following command:\n\n```bash\nflutter test\n```\n\nThe tests are adjusted for Clean Architecture, demonstrating how to test each\nlayer independently and ensuring the overall integrity of the application.\n\n## Full Implementation\n\nThis implementation is so simple that it can even fit in the README:\n\n```dart\nimport 'dart:async';\n\nimport 'package:flutter/material.dart';\n\n// Frameworks \u0026 Drivers Layer\nvoid main() =\u003e runApp(const MyApp());\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final CounterGateway gateway = FakeCounterGateway();\n    final IncrementCounter incrementCounter = IncrementCounterFakeImpl(\n      gateway,\n    );\n    final CounterPresenter presenter = CounterPresenter(incrementCounter);\n\n    return MaterialApp(\n      title: 'Flutter Demo',\n      debugShowCheckedModeBanner: false,\n      theme: ThemeData(\n        colorSchemeSeed: Colors.blue,\n      ),\n      home: MyHomePage(title: 'Flutter Demo Home Page', presenter: presenter),\n    );\n  }\n}\n\n// Interface Adapters Layer (Presenter)\nclass CounterPresenter {\n  CounterPresenter(this.incrementCounter) {\n    incrementCounter.counterStream.listen(_updateCounter);\n  }\n\n  final IncrementCounter incrementCounter;\n  Counter? _counter;\n  final StreamController\u003cCounter\u003e _controller = StreamController\u003cCounter\u003e();\n\n  Stream\u003cCounter\u003e get counterStream =\u003e _controller.stream;\n\n  void increment() {\n    if (_counter != null) {\n      incrementCounter.increment(_counter!);\n    }\n  }\n\n  void _updateCounter(Counter counter) {\n    _counter = counter;\n    _controller.add(_counter!);\n  }\n\n  void dispose() =\u003e _controller.close();\n}\n\n// Interface Adapters Layer (UI)\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({\n    required this.title,\n    required this.presenter,\n    super.key,\n  });\n\n  final String title;\n  final CounterPresenter presenter;\n\n  @override\n  State\u003cMyHomePage\u003e createState() =\u003e _MyHomePageState();\n}\n\nclass _MyHomePageState extends State\u003cMyHomePage\u003e {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: \u003cWidget\u003e[\n            const Text(\n              'You have pushed the button this many times:',\n            ),\n            StreamBuilder\u003cCounter\u003e(\n              stream: widget.presenter.counterStream,\n              initialData: const Counter(0),\n              builder: (BuildContext context, AsyncSnapshot\u003cCounter\u003e snapshot) {\n                return Text(\n                  '${snapshot.data?.value ?? 0}',\n                  style: Theme.of(context).textTheme.headlineMedium,\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: widget.presenter.increment,\n        tooltip: 'Increment',\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    widget.presenter.dispose();\n    super.dispose();\n  }\n}\n\n// Interface Adapters Layer (Fake Gateway Implementation)\nclass FakeCounterGateway implements CounterGateway {\n  FakeCounterGateway() {\n    _controller.add(_counter);\n  }\n\n  Counter _counter = const Counter(0);\n  final StreamController\u003cCounter\u003e _controller = StreamController\u003cCounter\u003e();\n\n  @override\n  Stream\u003cCounter\u003e watch() =\u003e _controller.stream;\n\n  @override\n  Future\u003cvoid\u003e saveCounter(Counter counter) async {\n    await Future\u003cvoid\u003e.delayed(Duration.zero);\n    _counter = counter;\n    _controller.add(_counter);\n  }\n}\n\n// UseCases Layer\nabstract interface class IncrementCounter {\n  const IncrementCounter();\n\n  void increment(Counter counter);\n\n  Stream\u003cCounter\u003e get counterStream;\n}\n\nabstract interface class CounterGateway {\n  const CounterGateway();\n\n  Stream\u003cCounter\u003e watch();\n\n  Future\u003cvoid\u003e saveCounter(Counter counter);\n}\n\nclass IncrementCounterFakeImpl implements IncrementCounter {\n  IncrementCounterFakeImpl(this.gateway) {\n    _init();\n  }\n\n  final CounterGateway gateway;\n\n  final StreamController\u003cCounter\u003e _controller = StreamController\u003cCounter\u003e();\n\n  Future\u003cvoid\u003e _init() async {\n    gateway.watch().listen((Counter counter) {\n      _controller.add(counter);\n    });\n  }\n\n  @override\n  void increment(Counter counter) async {\n    final Counter newCounter = counter.copyWith(value: counter.value + 1);\n    await gateway.saveCounter(newCounter);\n  }\n\n  @override\n  Stream\u003cCounter\u003e get counterStream =\u003e _controller.stream;\n}\n\n// Entities Layer\nclass Counter {\n  const Counter(this.value);\n\n  final int value;\n\n  Counter copyWith({int? value}) {\n    return Counter(value ?? this.value);\n  }\n}\n```\n\n### Screenshot:\n\n\u003c!--suppress CheckImageSize --\u003e\n\u003cimg src=\"screenshots/Android_Screenshot_20240805.png\" width=\"200\"  alt=\"screenshot\"\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturskyi%2Fflutter_clean_architecture_counter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fturskyi%2Fflutter_clean_architecture_counter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturskyi%2Fflutter_clean_architecture_counter/lists"}