Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.23.0
- Add "Tool Panel Factory" flutter example

## 0.22.0
- Add visitor pattern: "Shape Xml Export".

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ It contains **Dart** examples for all classic **GoF** design patterns.

# Implementation checklist:
- [ ] **Creation**
- [x] **Abstract Factory** [[Conceptual Gui Factory](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/abstract_factory/conceptual_gui_factory)]
- [x] **Abstract Factory** [[Conceptual Gui Factory](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/abstract_factory/conceptual_gui_factory)] [[Tool Panel Factory](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/abstract_factory/tool_panel_factory)]
- [x] **Builder** - [[Color Text Format](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/builder/color_text_format)]
- [x] **Factory Method** [[Conceptual Platform Dialog](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/factory_method/conceptual_platform_dialog)]
- [x] **Prototype** - [[Shapes](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/prototype/shapes)]
Expand Down
4 changes: 3 additions & 1 deletion bin/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../patterns/abstract_factory/tool_panel_factory/main.dart';
import '../patterns/observer/subscriber_flutter_widget/main.dart';
import '../patterns/adapter/flutter_adapter/main.dart';
import '../patterns/memento/memento_editor/main.dart';
Expand All @@ -13,11 +14,12 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Refactoring Guru: Flutter launcher',
theme: ThemeData(primarySwatch: Colors.pink),
initialRoute: '/memento/flutter_memento_editor',
initialRoute: '/abstract_factory/tool_panel',
routes: {
'/observer/subscriber_flutter_widget': (_) => SubscriberFlutterApp(),
'/adapter/flutter_adapter': (_) => FlutterAdapterApp(),
'/memento/flutter_memento_editor': (_) => FlutterMementoEditorApp(),
'/abstract_factory/tool_panel_factory': (_) => ToolPanelFactoryApp(),
},
);
}
Expand Down
49 changes: 49 additions & 0 deletions patterns/abstract_factory/tool_panel_factory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Abstract Factory Pattern
Abstract Factory is a creational design pattern that lets you produce families of related objects
without specifying their concrete classes.

Tutorial: [here](https://refactoring.guru/design-patterns/abstract-factory).

### Online demo:
Click on the picture to see the [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/abstract_factory/tool_panel_factory).

[![image](https://user-images.githubusercontent.com/8049534/168668992-369a1bab-9f97-4333-a20e-ffd06bf91b54.png)](https://refactoringguru.github.io/design-patterns-dart/#/abstract_factory/tool_panel_factory)

### Diagram:
![image](https://user-images.githubusercontent.com/8049534/168672053-73ae1c9c-8fad-45ae-9247-429f7b5da565.png)

### Client code (using the "createShape" method):
```dart
final app = App(
tools: Tools(
factories: [
TxtFactory(),
LineFactory(),
CircleFactory(),
TriangleFactory(),
StarFactory(),
],
),
);

class App {
void addShape(double x, double y) {
final newShape = activeFactory.createShape(x, y, activeColor);
shapes.add(newShape);
}
}

mixin IconBoxMixin implements FactoryTool {
Image? _icon;

@override
Image get icon => _icon!;

Future<void> updateIcon(Color color) async {
final shape = createShape(0, 0, color);
final pngBytes = await _pngImageFromShape(shape);
_icon = Image.memory(pngBytes);
}
}
```

21 changes: 21 additions & 0 deletions patterns/abstract_factory/tool_panel_factory/app/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import '../app/tools.dart';
import 'shapes.dart';

class App {
final Tools tools;
final Shapes shapes;

App({
required this.tools,
required this.shapes,
});

void addShape(double x, double y) {
final activeColor = tools.activeColor.value;
final activeFactory = tools.activeFactory.value!;

final newShape = activeFactory.createShape(x, y, activeColor);
newShape.centerToFit();
shapes.add(newShape);
}
}
25 changes: 25 additions & 0 deletions patterns/abstract_factory/tool_panel_factory/app/shapes.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'dart:collection';

import 'package:flutter/foundation.dart';

import '../pattern/shape.dart';

class Shapes with IterableMixin<Shape> {
final List<Shape> _shapes;

Shapes(this._shapes);

void add(Shape shape) {
_shapes.add(shape);
onAddShapeEvent._emit();
}

@override
Iterator<Shape> get iterator => _shapes.iterator;

final onAddShapeEvent = Event();
}

class Event extends ChangeNotifier {
void _emit() => notifyListeners();
}
40 changes: 40 additions & 0 deletions patterns/abstract_factory/tool_panel_factory/app/tools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:async';
import 'dart:ui';

import 'package:flutter/foundation.dart';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';

class Tools {
final List<ToolFactory> factories;
final List<Color> colors;

final activeFactory = ValueNotifier<ToolFactory?>(null);

final activeColor = ValueNotifier(Color(0x0FFFFFFFF));

Future<bool> get iconsReady => _iconsInitCompleter.future;

Tools({required this.factories, required this.colors}) {
_initIconsFromShapes();

if (factories.isNotEmpty) {
activeFactory.value = factories.first;
}

if (colors.isNotEmpty) {
activeColor.value = colors.first;
}
}

final _iconsInitCompleter = Completer<bool>();

void _initIconsFromShapes() async {
await Future.wait([
for (final factory in factories)
(factory as IconBoxMixin).updateIcon(activeColor.value),
]);
_iconsInitCompleter.complete(Future.value(true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:ui';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';
import '../shapes/circle_shape.dart';
import '../pattern/shape.dart';

class CircleFactory extends ToolFactory with IconBoxMixin {
@override
Shape createShape(double x, double y, Color color) {
return CircleShape(
radius: 50,
x: x,
y: y,
color: color,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:ui';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';
import '../pattern/shape.dart';
import '../shapes/line_shape.dart';

class LineFactory extends ToolFactory with IconBoxMixin {
@override
Shape createShape(double x, double y, Color color) {
return LineShape(
length: 100,
x: x,
y: y,
color: color,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:ui';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';
import '../pattern/shape.dart';
import '../shapes/star_shape.dart';

class StarFactory extends ToolFactory with IconBoxMixin {
@override
Shape createShape(double x, double y, Color color) {
return StarShape(
radius: 80,
x: x,
y: y,
color: color,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:ui';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';
import '../pattern/shape.dart';
import '../shapes/triangle_shape.dart';

class TriangleFactory extends ToolFactory with IconBoxMixin {
@override
Shape createShape(double x, double y, Color color) {
return TriangleShape(
sideLength: 120,
x: x,
y: y,
color: color,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'dart:ui';

import '../mixin/icon_box_mixin.dart';
import '../pattern/tool_factory.dart';
import '../shapes/txt_shape.dart';
import '../pattern/shape.dart';

class TxtFactory extends ToolFactory with IconBoxMixin {
@override
Shape createShape(double x, double y, Color color) {
return Txt(
text: 'Text',
fontSize: 50,
x: x,
y: y,
color: color,
);
}
}
55 changes: 55 additions & 0 deletions patterns/abstract_factory/tool_panel_factory/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';

import 'app/app.dart';
import 'app/shapes.dart';
import 'app/tools.dart';
import 'factories/circle_factory.dart';
import 'factories/line_factory.dart';
import 'factories/star_factory.dart';
import 'factories/triangle_factory.dart';
import 'factories/txt_factory.dart';
import 'widgets/drawing_board.dart';
import 'widgets/tool_panel.dart';

class ToolPanelFactoryApp extends StatefulWidget {
const ToolPanelFactoryApp({Key? key}) : super(key: key);

@override
_ToolPanelFactoryAppState createState() => _ToolPanelFactoryAppState();
}

class _ToolPanelFactoryAppState extends State<ToolPanelFactoryApp> {
final app = App(
shapes: Shapes([]),
tools: Tools(
factories: [
TxtFactory(),
LineFactory(),
CircleFactory(),
TriangleFactory(),
StarFactory(),
],
colors: [
Colors.white,
Colors.green,
Colors.blue,
Colors.yellow,
],
),
);

@override
Widget build(BuildContext context) {
return Stack(
children: [
DrawingBoard(
shapes: app.shapes,
onClick: app.addShape,
),
ToolPanel(
tools: app.tools,
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/material.dart';

import '../pattern/tool_factory.dart';
import '../pattern/shape.dart';

mixin IconBoxMixin implements ToolFactory {
Image? _icon;

@override
Image get icon => _icon!;

Future<void> updateIcon(Color color) async {
final shape = createShape(0, 0, color);
final pngBytes = await _pngImageFromShape(shape);
_icon = Image.memory(
pngBytes,
fit: BoxFit.none,
);
}

Future<Uint8List> _pngImageFromShape(Shape shape) async {
final iconSize = 32.0;

final rec = PictureRecorder();
final can = Canvas(
rec,
Rect.fromLTWH(0, 0, iconSize, iconSize),
);

_scaleTo(can, shape, iconSize);
shape.paint(can);

final image = await rec.endRecording().toImage(
iconSize.toInt(),
iconSize.toInt(),
);
final bytes = await image.toByteData(format: ImageByteFormat.png);

if (bytes == null) {
throw 'Bytes is empty.';
}

return bytes.buffer.asUint8List();
}

void _scaleTo(Canvas can, Shape shape, double iconSize) {
var xMove = 0.0;
var yMove = 0.0;
late double w;
late double h;

if (shape.width >= shape.height) {
yMove = (shape.width - shape.height);
w = iconSize / shape.width;
h = iconSize / (shape.height + yMove);
yMove /= 2;
} else {
xMove = (shape.height - shape.width);
w = iconSize / (shape.width + xMove);
h = iconSize / shape.height;
xMove /= 2;
}

can.scale(w, h);
can.translate(xMove, yMove);
}
}
Loading