Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/turskyi/flutter_onion_architecture_counter
A refactored version of the default Flutter counter app, demonstrating Onion 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.
https://github.com/turskyi/flutter_onion_architecture_counter
architecture-patterns clean-architecture clean-code counter-app dart dependency-injection example-project flutter flutter-examples onion-architecture reactive-programming software-architecture state-management stream streams
Last synced: 27 days ago
JSON representation
A refactored version of the default Flutter counter app, demonstrating Onion 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.
- Host: GitHub
- URL: https://github.com/turskyi/flutter_onion_architecture_counter
- Owner: Turskyi
- Created: 2024-08-06T01:37:19.000Z (5 months ago)
- Default Branch: master
- Last Pushed: 2024-08-08T00:10:20.000Z (5 months ago)
- Last Synced: 2024-08-20T14:26:13.584Z (4 months ago)
- Topics: architecture-patterns, clean-architecture, clean-code, counter-app, dart, dependency-injection, example-project, flutter, flutter-examples, onion-architecture, reactive-programming, software-architecture, state-management, stream, streams
- Language: C++
- Homepage: https://dartpad.dev/?id=162cc336713fefc21013d2dae0382adf
- Size: 1.62 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
[![style: flutter lints](https://img.shields.io/badge/style-flutter__lints-blue)](https://pub.dev/packages/flutter_lints)
[![Code Quality](https://github.com/Turskyi/flutter_onion_architecture_counter/actions/workflows/code-quality-tests.yml/badge.svg?branch=master&event=push)](https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md)
[![codecov](https://codecov.io/gh/Turskyi/flutter_onion_architecture_counter/graph/badge.svg?token=CHHAH6OHDE)](https://codecov.io/gh/Turskyi/flutter_onion_architecture_counter)# Flutter Onion Architecture Counter With Stream
This project is a refactored version of the
[default Flutter counter app](https://dartpad.dev/?sample=counter),
demonstrating how it could be implemented in a production environment using
Onion Architecture, originally introduced by
[Jeffrey Palermo](https://jeffreypalermo.com/about/) in his article
[The Onion Architecture](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/).The default "counter" app is often criticized for its simplicity and lack of
real-world applicability. This example showcases a more maintainable and
scalable approach, incorporating immutability, dependency injection, and
reactive state management
using [streams](https://dart.dev/libraries/async/using-streams).
## Architecture Overview
The project follows the four main layers of Onion Architecture:
1. **Domain Model**: Core business logic and models.
2. **Domain Services**: Business rules and operations.
3. **Application Services**: Application-specific logic and orchestration.
4. **Outermost Layer**: Includes User Interface, Infrastructure (DB and/or WS),
and Tests.### Domain Model Layer
Contains the `Counter` entity. This layer does not depend on anything else,
which is clear from the imports in the class.### Domain Services Layer
Contains the `IncrementCounter` interface and its implementation
`IncrementCounterFakeImpl`. This layer depends only on the Domain Model.### Application Services Layer
Contains the `CounterPresenter` which manages the state and business logic.
This layer depends on both Domain Services and Domain Model.### Outermost Layer
- **User Interface Component**: Contains the `MyHomePage` widget and the
`main` function with `MyApp` widget.
- **Infrastructure Component**: Contains the `CounterDataSource` interface and
its fake implementation `FakeCounterDataSource`, which uses a `Stream` for the
`watch` method.
- **Tests**: Adjusted for Onion Architecture, highlighting their importance as
a component of the outermost layer.The Infrastructure, User Interface, and Tests components have access to all
inner layers.### Note on Layer Separation
For the sake of simplicity, the inner layers are not decoupled into separate
packages in this example. In a production environment, it is essential to
enforce the dependency flow by separating these layers into different
packages. This ensures that, for example, the Domain Model layer cannot access
the User Interface layer.### Project Structure
The simplified structure of the project is as follows:
```
lib/
├── main.dart
├── user_interface/
├── infrastructure/
└── core/
├── application_services/
└── domain/
├── model/
└── services/
```## Getting Started
To get started with this project, clone the repository and run the following
commands:```bash
flutter pub get
flutter run
```You can also test the implementation directly on DartPad:
https://dartpad.dev/?id=162cc336713fefc21013d2dae0382adf## Running Tests
To run the tests, use the following command:
```bash
flutter test
```The tests are adjusted for Onion Architecture, demonstrating how to test each
layer independently and ensuring the overall integrity of the application.## Full Implementation
This implementation is so simple that it can even fit in the README:
```dart
import 'dart:async';import 'package:flutter/material.dart';
// Domain Model Layer
class Counter {
const Counter(this.value);final int value;
Counter copyWith({int? value}) {
return Counter(value ?? this.value);
}
}// Domain Services Layer
abstract interface class IncrementCounter {
const IncrementCounter();void increment(Counter counter);
Stream get counterStream;
}class IncrementCounterFakeImpl implements IncrementCounter {
IncrementCounterFakeImpl(this.dataSource) {
_init();
}final CounterDataSource dataSource;
final StreamController _controller = StreamController();Future _init() async {
dataSource.watch().listen((Counter counter) {
_controller.add(counter);
});
}@override
void increment(Counter counter) async {
final Counter newCounter = counter.copyWith(value: counter.value + 1);
await dataSource.saveCounter(newCounter);
}@override
Stream get counterStream => _controller.stream;
}// Application Services Layer
class CounterPresenter {
CounterPresenter(this.incrementCounter) {
incrementCounter.counterStream.listen(_updateCounter);
}final IncrementCounter incrementCounter;
Counter? _counter;
final StreamController _controller = StreamController();Stream get counterStream => _controller.stream;
void increment() {
if (_counter != null) {
incrementCounter.increment(_counter!);
}
}void _updateCounter(Counter counter) {
_counter = counter;
_controller.add(_counter!);
}void dispose() => _controller.close();
}// Outermost layer.
// User Interface Component
class MyHomePage extends StatefulWidget {
const MyHomePage({
required this.title,
required this.presenter,
super.key,
});final String title;
final CounterPresenter presenter;@override
State createState() => _MyHomePageState();
}class _MyHomePageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: widget.presenter.counterStream,
initialData: const Counter(0),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Text(
'${snapshot.data?.value ?? 0}',
style: Theme
.of(context)
.textTheme
.headlineMedium,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: widget.presenter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}@override
void dispose() {
widget.presenter.dispose();
super.dispose();
}
}class MyApp extends StatelessWidget {
const MyApp({required this.presenter, super.key});final CounterPresenter presenter;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Onion Architecture Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
),
home: MyHomePage(
title: 'Onion Architecture Demo Home Page',
presenter: presenter,
),
);
}
}void main() {
final FakeCounterDataSource dataSource = FakeCounterDataSource();
final IncrementCounter incrementCounter = IncrementCounterFakeImpl(
dataSource,
);
final CounterPresenter presenter = CounterPresenter(incrementCounter);
runApp(MyApp(presenter: presenter));
}// Infrastructure Component
abstract interface class CounterDataSource {
const CounterDataSource();Stream watch();
Future saveCounter(Counter counter);
}class FakeCounterDataSource implements CounterDataSource {
FakeCounterDataSource() {
_controller.add(_counter);
}Counter _counter = const Counter(0);
final StreamController _controller = StreamController();@override
Stream watch() => _controller.stream;@override
Future saveCounter(Counter counter) async {
await Future.delayed(Duration.zero);
_counter = counter;
_controller.add(_counter);
}
}
```### Screenshot: