Python Streaming 서버 예제 (Flask와 Redis 사용)

|

Streaming 서버 예제 (Flask와 Redis 사용)

브라우저에서 http://localhost:8001/message에 접속하면 데이터가 스트리밍 되는 것을 확인할 수 있습니다.

예제 코드

import json

import flask
import redis
from flask import Flask

app = Flask(__name__)
app.debug = True


def stream_message(channel):
    r = redis.Redis()
    p = r.pubsub()
    p.subscribe(channel)
    for message in p.listen():
        if message['type'] == 'message':
            yield 'data: ' + json.dumps(message['data'].decode()) + '\n\n'


@app.route('/message', methods=['GET'])
def get_messages():
    return flask.Response(
        flask.stream_with_context(stream_message('snowdeer_channel')),
        mimetype='text/event-stream'
    )


if __name__ == '__main__':
    app.run(port=8001, use_reloader=False)

publisher.py

import redis
import datetime

r = redis.Redis(host="localhost", port=6379, db=0)

msg = f"[{datetime.datetime.now()}] hello, snowdeer +___+"
r.publish(channel="snowdeer_channel",
          message=msg)

print(f"Message is sent !!\n{msg}")

Python asyncio 및 Coroutine 예제

|

asyncio 및 Coroutine 예제

asyncioselect와 동일한 방식으로 동작하는 이벤트 루프 모듈입니다. asyncio로 이벤트 루프를 생성하고, 어플리케이션은 특정 이벤트가 발생했을 때 호출할 함수를 등록합니다. 이러한 유형의 함수를 코루틴(Coroutine)이라고 합니다. 코루틴은 호출한 쪽에 제어를 되돌려 줄 수 있는 특별한 형태의 함수로 호출한 측에서 이벤트 루프를 계속 실행할 수 있게 합니다.

코루틴은 yield 명령어를 이용해서 호출한 측에 제어권을 돌려주는 제너레이트와 동일하게 동작합니다.

간단한 예제

import asyncio
import time


async def hello() -> str:
    print('hello, snowdeer')

    for i in range(0, 5):
        print(f'hello({i})')
        time.sleep(1)

    return 'snowdeer'


hello_coroutine = hello()

print('* #1')
print(hello_coroutine)

print('* #2')
event_loop = asyncio.get_event_loop()
try:
    print("waiting event loop ...")
    result = event_loop.run_until_complete(hello_coroutine)
finally:
    event_loop.close()

print(f'result: {result}')

결과는 다음과 같습니다.

* #1
<coroutine object hello at 0x100845c40>
* #2
waiting event loop ...
hello, snowdeer
hello(0)
hello(1)
hello(2)
hello(3)
hello(4)
result: snowdeer

Coroutine에서 다른 Coroutine 호출하는 예제

import asyncio
import time


async def get_name() -> str:
    print('get_name() is called')

    for i in range(0, 3):
        print(f'get_name({i}) ...')
        time.sleep(1)

    return 'snowdeer'


async def hello() -> str:
    print('hello() is called')

    name = await get_name()

    for i in range(0, 3):
        print(f'hello, {name} ({i}) ...')
        time.sleep(1)

    return name


hello_coroutine = hello()

print('* #1')
print(hello_coroutine)

print('* #2')
event_loop = asyncio.get_event_loop()
try:
    print("waiting event loop ...")
    result = event_loop.run_until_complete(hello_coroutine)
finally:
    event_loop.close()

print(f'result: {result}')
* #1
<coroutine object hello at 0x1054c1c40>
* #2
waiting event loop ...
hello() is called
get_name() is called
get_name(0) ...
get_name(1) ...
get_name(2) ...
hello, snowdeer (0) ...
hello, snowdeer (1) ...
hello, snowdeer (2) ...
result: snowdeer

name = await get_name() 코드에서 await 키워드는 get_name()이라는 코루틴을 이벤트 루프에 등록하고 제어권을 이벤트 루프에 넘깁니다. 이벤트 루프는 제어권을 받아서 get_name() 코루틴을 실행한 다음 작업이 완료되면 이벤트 루프가 다시 기존의 hello() 코루틴 실행을 이어갑니다.

await의 또 다른 예제

import asyncio
import time


async def loop1():
    print('loop1() is called')

    for i in range(0, 10):
        print(f'loop1({i}) ...')
        await time.sleep(1)


async def loop2():
    print('loop2() is called')

    for i in range(0, 10):
        print(f'loop2({i}) ...')
        await time.sleep(1)


event_loop = asyncio.get_event_loop()
try:
    print("waiting event loop ...")
    result = event_loop.run_until_complete(
        asyncio.gather(
            loop1(),
            loop2(),
        )
    )
finally:
    event_loop.close()

와 같은 코드를 실행하면 다음과 같은 오류가 발생합니다.

waiting event loop ...
loop1() is called
loop1(0) ...
loop2() is called
loop2(0) ...
Traceback (most recent call last):
  File "/Users/snowdeer/Workspace/snowdeer/python_scalability/asyncio_example.py", line 24, in <module>
    result = event_loop.run_until_complete(
  File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/Users/snowdeer/Workspace/snowdeer/python_scalability/asyncio_example.py", line 10, in loop1
    await time.sleep(1)
TypeError: object NoneType can't be used in 'await' expression

따라서 이런 경우는 time.sleep()가 아니라 asyncio.sleep()를 사용해야 합니다. asyncio.sleep()time.sleep()와 달리 비동기 방식이기 때문에 지정된 시간까지 다른 일을 처리할 수 있습니다.

import asyncio
import time


async def loop1():
    print('loop1() is called')

    for i in range(0, 10):
        print(f'loop1({i}) ...')
        await asyncio.sleep(1)


async def loop2():
    print('loop2() is called')

    for i in range(0, 10):
        print(f'loop2({i}) ...')
        await asyncio.sleep(1)


event_loop = asyncio.get_event_loop()
try:
    print("waiting event loop ...")
    result = event_loop.run_until_complete(
        asyncio.gather(
            loop1(),
            loop2(),
        )
    )
finally:
    event_loop.close()

Flutter Canvas 그리기 예제 - (8) Node, Edge는 Model로 분리

|

Node, Edge는 Model로 분리

drawing_model.dart

import 'dart:math';

import 'package:snowdeer_canvas_example/drag_and_drop/edge.dart';
import 'package:snowdeer_canvas_example/drag_and_drop/node.dart';

class DrawingModel {
  final List<Node> _nodeList = List.empty(growable: true);
  final List<Edge> _edgeList = List.empty(growable: true);

  DrawingModel() {
    _nodeList.clear();
    _edgeList.clear();
  }

  void addNode(Node node) {
    _nodeList.add(node);
  }

  void addEdge(Edge edge) {
    _edgeList.add(edge);
  }

  Node getNode(x, y) {
    const radius = 30.0;
    for (final node in _nodeList) {
      final distance =
          sqrt((node.x - x) * (node.x - x) + (node.y - y) * (node.y - y));
      if (distance <= radius) {
        return node;
      }
    }
    return null;
  }

  List getNodeList() {
    return _nodeList;
  }

  List getEdgeList() {
    return _edgeList;
  }
}

draggable_painter.dart

import 'dart:math';

import 'package:arrow_path/arrow_path.dart';
import 'package:flutter/material.dart';

import 'drawing_model.dart';

class DraggablePainter extends CustomPainter {
  static const gridWidth = 50.0;
  static const gridHeight = 50.0;
  static const radius = 30.0;

  var _width = 0.0;
  var _height = 0.0;

  final double offsetX;
  final double offsetY;

  final DrawingModel model;

  DraggablePainter(this.model, this.offsetX, this.offsetY);

  void _drawBackground(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white70
      ..isAntiAlias = true;

    Rect rect = Rect.fromLTWH(0, 0, _width, _height);
    canvas.drawRect(rect, paint);
  }

  void _drawGrid(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.grey
      ..isAntiAlias = true;

    final gridRect = Rect.fromLTWH(
        offsetX % gridWidth - gridWidth,
        offsetY % gridHeight - gridHeight,
        _width + gridWidth,
        _height + gridHeight);

    final rows = _height / gridHeight;
    final cols = _width / gridWidth;

    for (int r = -1; r <= rows; r++) {
      final y = r * gridHeight + gridRect.top;
      final p1 = Offset(gridRect.left, y);
      final p2 = Offset(gridRect.right, y);

      canvas.drawLine(p1, p2, paint);
    }

    for (int c = -1; c <= cols; c++) {
      final x = c * gridWidth + gridRect.left;
      final p1 = Offset(x, gridRect.top);
      final p2 = Offset(x, gridRect.bottom);

      canvas.drawLine(p1, p2, paint);
    }
  }

  Offset _getCenterPosOfNode(nodeId) {
    for (final node in model.getNodeList()) {
      if (nodeId == node.id) {
        return Offset(node.x, node.y);
      }
    }
    return null;
  }

  void _drawEdges(Canvas canvas) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.black
      ..strokeWidth = 2
      ..isAntiAlias = true;

    for (final edge in model.getEdgeList()) {
      final fromPos = _getCenterPosOfNode(edge.fromId);
      final toPos = _getCenterPosOfNode(edge.toId);

      if ((fromPos != null) && (toPos != null)) {
        final distance =
            Offset(toPos.dx - fromPos.dx, toPos.dy - fromPos.dy).distance -
                radius;
        final theta = atan2((toPos.dy - fromPos.dy), (toPos.dx - fromPos.dx));
        final targetX = fromPos.dx + distance * cos(theta);
        final targetY = fromPos.dy + distance * sin(theta);

        var path = Path();
        path.moveTo(fromPos.dx, fromPos.dy);
        path.lineTo(targetX, targetY);
        path = ArrowPath.make(path: path);
        canvas.drawPath(path, paint);
      }
    }
  }

  void _drawNodes(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.amber
      ..isAntiAlias = true;

    const textStyle = TextStyle(
      color: Colors.black,
      fontSize: 14,
    );

    for (final node in model.getNodeList()) {
      final c = Offset(node.x, node.y);
      canvas.drawCircle(c, radius, paint);
      _drawText(canvas, node.x, node.y, node.name, textStyle);
    }
  }

  void _drawText(Canvas canvas, centerX, centerY, text, style) {
    final textSpan = TextSpan(
      text: text,
      style: style,
    );

    final textPainter = TextPainter()
      ..text = textSpan
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center
      ..layout();

    final xCenter = (centerX - textPainter.width / 2);
    final yCenter = (centerY - textPainter.height / 2);
    final offset = Offset(xCenter, yCenter);

    textPainter.paint(canvas, offset);
  }

  void _drawCanvas(Canvas canvas) {
    _drawBackground(canvas);
    _drawGrid(canvas);

    canvas.save();
    canvas.translate(offsetX, offsetY);

    _drawEdges(canvas);
    _drawNodes(canvas);

    canvas.restore();
  }

  @override
  void paint(Canvas canvas, Size size) {
    _width = size.width;
    _height = size.height;

    _drawCanvas(canvas);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

draggable_canvas.dart

import 'package:flutter/material.dart';
import 'package:snowdeer_canvas_example/drag_and_drop/drawing_model.dart';

import 'draggable_painter.dart';
import 'edge.dart';
import 'node.dart';

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

  @override
  State createState() => DraggableObjectPageState();
}

class DraggableObjectPageState extends State<DraggableObjectPage> {
  final model = DrawingModel();

  var offsetX = 0.0;
  var offsetY = 0.0;

  var preX = 0.0;
  var preY = 0.0;

  Node currentNode;

  DraggableObjectPageState() {
    initNodes();
    initEdges();
  }

  void initNodes() {
    model.addNode(Node(1, "Node\n1", 150.0, 210.0));
    model.addNode(Node(2, "Node\n2", 220.0, 40.0));
    model.addNode(Node(3, "Node\n3", 440.0, 240.0));
    model.addNode(Node(4, "Node\n4", 640.0, 150.0));
    model.addNode(Node(5, "Node\n5", 480.0, 350.0));
  }

  void initEdges() {
    model.addEdge(Edge(1, 2));
    model.addEdge(Edge(3, 2));
    model.addEdge(Edge(5, 3));
    model.addEdge(Edge(5, 4));
  }

  void _handlePanDown(details) {
    final x = details.localPosition.dx;
    final y = details.localPosition.dy;

    final node = model.getNode(x - offsetX, y - offsetY);
    if (node != null) {
      currentNode = node;
    } else {
      currentNode = null;
    }

    preX = x;
    preY = y;
  }

  void _handlePanUpdate(details) {
    final dx = details.localPosition.dx - preX;
    final dy = details.localPosition.dy - preY;

    if (currentNode != null) {
      setState(() {
        currentNode.x = currentNode.x + dx;
        currentNode.y = currentNode.y + dy;
      });
    } else {
      setState(() {
        offsetX = offsetX + dx;
        offsetY = offsetY + dy;
      });
    }

    preX = details.localPosition.dx;
    preY = details.localPosition.dy;
  }

  void _handleLongPressStart(details) {
    final x = details.localPosition.dx;
    final y = details.localPosition.dy;

    final node = model.getNode(x - offsetX, y - offsetY);

    if (node == null) {
      return;
    }

    showDialog(
      context: context,
      builder: (BuildContext context) {
        // return object of type Dialog
        return AlertDialog(
          title: Text(node.name),
          content: const Text("Edit node."),
          actions: [
            TextButton(
              child: const Text("Ok"),
              onPressed: () {
                setState(() {
                  node.name = node.name + '+';
                });
                Navigator.pop(context);
              },
            ),
          ],
        );
      },
    );
  }

  void _handleLongPressMoveUpdate(details) {}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onPanDown: _handlePanDown,
        onPanUpdate: _handlePanUpdate,
        onLongPressStart: _handleLongPressStart,
        onLongPressMoveUpdate: _handleLongPressMoveUpdate,
        child: CustomPaint(
          child: Container(),
          painter: DraggablePainter(model, offsetX, offsetY),
        ),
      ),
    );
  }
}

Flutter Canvas 그리기 예제 - (7) Node 사이 Edge(Arrow) 그리기

|

Node 사이 Edge 그리기

image

사실 일일이 직접 그려도 되지만, 화살표 끝부분 처리가 너무 귀찮아서 오픈 소스를 활용했습니다. 활용한 오픈소스는 arrow_path 2.0.0입니다.

아래 그림과 다양한 모양의 화살표를 만들 수 있습니다. 하지만, 어차피 경로(Path)는 직접 구해야 합니다.

image

pubspec.yaml

아래와 같이 pubspec.yaml 파일의 dev_dependencies 항목 아래에 arrow_path: ^2.0.0를 추가해줍니다. 그리고 flutter pub get 명령어를 실행해줍니다.

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^1.0.0
  arrow_path: ^2.0.0

node.dart

기존 코드와 거의 같습니다. idint 형으로 변경하긴 했는데, String으로 해도 코드에 큰 차이가 없습니다.

class Node {
  int id;
  String name;
  double x;
  double y;

  Node(this.id, this.name, this.x, this.y);
}

edge.dart

class Edge {
  int fromId;
  int toId;

  Edge(this.fromId, this.toId);
}

draggable_painter.dart

import 'dart:math';

import 'package:arrow_path/arrow_path.dart';
import 'package:flutter/material.dart';

import 'edge.dart';
import 'node.dart';

class DraggablePainter extends CustomPainter {
  static const gridWidth = 50.0;
  static const gridHeight = 50.0;

  var _width = 0.0;
  var _height = 0.0;

  final double offsetX;
  final double offsetY;
  final List<Node> nodeList;
  final List<Edge> edgeList;

  final radius = 30.0;

  DraggablePainter(this.nodeList, this.edgeList, this.offsetX, this.offsetY);

  void _drawBackground(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white70
      ..isAntiAlias = true;

    Rect rect = Rect.fromLTWH(0, 0, _width, _height);
    canvas.drawRect(rect, paint);
  }

  void _drawGrid(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.grey
      ..isAntiAlias = true;

    final gridRect = Rect.fromLTWH(
        offsetX % gridWidth - gridWidth,
        offsetY % gridHeight - gridHeight,
        _width + gridWidth,
        _height + gridHeight);

    final rows = _height / gridHeight;
    final cols = _width / gridWidth;

    for (int r = -1; r <= rows; r++) {
      final y = r * gridHeight + gridRect.top;
      final p1 = Offset(gridRect.left, y);
      final p2 = Offset(gridRect.right, y);

      canvas.drawLine(p1, p2, paint);
    }

    for (int c = -1; c <= cols; c++) {
      final x = c * gridWidth + gridRect.left;
      final p1 = Offset(x, gridRect.top);
      final p2 = Offset(x, gridRect.bottom);

      canvas.drawLine(p1, p2, paint);
    }
  }

  Offset _getCenterPosOfNode(nodeId) {
    for (final node in nodeList) {
      if (nodeId == node.id) {
        return Offset(node.x, node.y);
      }
    }
    return null;
  }

  void _drawEdges(Canvas canvas) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.black
      ..strokeWidth = 2
      ..isAntiAlias = true;

    for (final edge in edgeList) {
      final fromPos = _getCenterPosOfNode(edge.fromId);
      final toPos = _getCenterPosOfNode(edge.toId);

      if ((fromPos != null) && (toPos != null)) {
        final distance =
            Offset(toPos.dx - fromPos.dx, toPos.dy - fromPos.dy).distance -
                radius;
        final theta = atan2((toPos.dy - fromPos.dy), (toPos.dx - fromPos.dx));
        final targetX = fromPos.dx + distance * cos(theta);
        final targetY = fromPos.dy + distance * sin(theta);

        var path = Path();
        path.moveTo(fromPos.dx, fromPos.dy);
        path.lineTo(targetX, targetY);
        path = ArrowPath.make(path: path);
        canvas.drawPath(path, paint);
      }
    }
  }

  void _drawNodes(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.amber
      ..isAntiAlias = true;

    const textStyle = TextStyle(
      color: Colors.black,
      fontSize: 14,
    );

    for (int i = 0; i < nodeList.length; i++) {
      final c = Offset(nodeList[i].x, nodeList[i].y);
      canvas.drawCircle(c, radius, paint);
      _drawText(
          canvas, nodeList[i].x, nodeList[i].y, nodeList[i].name, textStyle);
    }
  }

  void _drawText(Canvas canvas, centerX, centerY, text, style) {
    final textSpan = TextSpan(
      text: text,
      style: style,
    );

    final textPainter = TextPainter()
      ..text = textSpan
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center
      ..layout();

    final xCenter = (centerX - textPainter.width / 2);
    final yCenter = (centerY - textPainter.height / 2);
    final offset = Offset(xCenter, yCenter);

    textPainter.paint(canvas, offset);
  }

  void _drawCanvas(Canvas canvas) {
    _drawBackground(canvas);
    _drawGrid(canvas);

    canvas.save();
    canvas.translate(offsetX, offsetY);

    _drawEdges(canvas);
    _drawNodes(canvas);

    canvas.restore();
  }

  @override
  void paint(Canvas canvas, Size size) {
    _width = size.width;
    _height = size.height;

    _drawCanvas(canvas);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

draggable_canvas.dart

import 'package:flutter/material.dart';

import 'draggable_painter.dart';
import 'edge.dart';
import 'node.dart';

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

  @override
  State createState() => DraggableObjectPageState();
}

class DraggableObjectPageState extends State<DraggableObjectPage> {
  final List<Node> nodeList = List.empty(growable: true);
  final List<Edge> edgeList = List.empty(growable: true);

  var offsetX = 0.0;
  var offsetY = 0.0;

  var preX = 0.0;
  var preY = 0.0;

  Node currentNode;

  DraggableObjectPageState() {
    initNodes();
    initEdges();
  }

  void initNodes() {
    nodeList.clear();
    nodeList.add(Node(1, "Node\n1", 150.0, 210.0));
    nodeList.add(Node(2, "Node\n2", 220.0, 40.0));
    nodeList.add(Node(3, "Node\n3", 440.0, 240.0));
    nodeList.add(Node(4, "Node\n4", 640.0, 150.0));
    nodeList.add(Node(5, "Node\n5", 480.0, 350.0));
  }

  void initEdges() {
    edgeList.clear();
    edgeList.add(Edge(1, 2));
    edgeList.add(Edge(3, 2));
    edgeList.add(Edge(5, 3));
    edgeList.add(Edge(5, 4));
  }

  Node getNode(x, y) {
    const radius = 30.0;
    for (final node in nodeList) {
      final c = Offset(node.x - x + offsetX, node.y - y + offsetY);
      if (c.distance <= radius) {
        return node;
      }
    }
    return null;
  }

  void _handlePanDown(details) {
    final x = details.localPosition.dx;
    final y = details.localPosition.dy;

    final node = getNode(x, y);
    if (node != null) {
      currentNode = node;
    } else {
      currentNode = null;
    }

    preX = x;
    preY = y;
  }

  void _handlePanUpdate(details) {
    final dx = details.localPosition.dx - preX;
    final dy = details.localPosition.dy - preY;

    if (currentNode != null) {
      setState(() {
        currentNode.x = currentNode.x + dx;
        currentNode.y = currentNode.y + dy;
      });
    } else {
      setState(() {
        offsetX = offsetX + dx;
        offsetY = offsetY + dy;
      });
    }

    preX = details.localPosition.dx;
    preY = details.localPosition.dy;
  }

  void _handleLongPressDown(details) {}

  void _handleLongPressMoveUpdate(details) {}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onPanDown: _handlePanDown,
        onPanUpdate: _handlePanUpdate,
        onLongPressDown: _handleLongPressDown,
        onLongPressMoveUpdate: _handleLongPressMoveUpdate,
        child: CustomPaint(
          child: Container(),
          painter: DraggablePainter(nodeList, edgeList, offsetX, offsetY),
        ),
      ),
    );
  }
}

Flutter Canvas 그리기 예제 - (6) 드래그로 Canvas 내 Object 이동시키기

|

드래그로 Canvas 내 Object 이동시키기

node.dart

class Node {
  String id;
  String name;
  double x;
  double y;

  Node(this.id, this.name, this.x, this.y);
}

draggable_painter.dart

import 'package:flutter/material.dart';

import 'node.dart';

class DraggablePainter extends CustomPainter {
  static const gridWidth = 50.0;
  static const gridHeight = 50.0;

  var _width = 0.0;
  var _height = 0.0;

  final double offsetX;
  final double offsetY;
  final List<Node> nodeList;

  DraggablePainter(this.nodeList, this.offsetX, this.offsetY);

  void _drawBackground(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white70
      ..isAntiAlias = true;

    Rect rect = Rect.fromLTWH(0, 0, _width, _height);
    canvas.drawRect(rect, paint);
  }

  void _drawGrid(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.grey
      ..isAntiAlias = true;

    final gridRect = Rect.fromLTWH(
        offsetX % gridWidth - gridWidth,
        offsetY % gridHeight - gridHeight,
        _width + gridWidth,
        _height + gridHeight);

    final rows = _height / gridHeight;
    final cols = _width / gridWidth;

    for (int r = -1; r <= rows; r++) {
      final y = r * gridHeight + gridRect.top;
      final p1 = Offset(gridRect.left, y);
      final p2 = Offset(gridRect.right, y);

      canvas.drawLine(p1, p2, paint);
    }

    for (int c = -1; c <= cols; c++) {
      final x = c * gridWidth + gridRect.left;
      final p1 = Offset(x, gridRect.top);
      final p2 = Offset(x, gridRect.bottom);

      canvas.drawLine(p1, p2, paint);
    }
  }

  void _drawNodes(Canvas canvas) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.amber
      ..isAntiAlias = true;

    const textStyle = TextStyle(
      color: Colors.black,
      fontSize: 14,
    );

    const radius = 30.0;
    for (int i = 0; i < nodeList.length; i++) {
      final c = Offset(nodeList[i].x, nodeList[i].y);
      canvas.drawCircle(c, radius, paint);
      _drawText(
          canvas, nodeList[i].x, nodeList[i].y, nodeList[i].name, textStyle);
    }
  }

  void _drawText(Canvas canvas, centerX, centerY, text, style) {
    final textSpan = TextSpan(
      text: text,
      style: style,
    );

    final textPainter = TextPainter()
      ..text = textSpan
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center
      ..layout();

    final xCenter = (centerX - textPainter.width / 2);
    final yCenter = (centerY - textPainter.height / 2);
    final offset = Offset(xCenter, yCenter);

    textPainter.paint(canvas, offset);
  }

  void _drawCanvas(Canvas canvas) {
    _drawBackground(canvas);
    _drawGrid(canvas);

    canvas.save();
    canvas.translate(offsetX, offsetY);
    _drawNodes(canvas);

    canvas.restore();
  }

  @override
  void paint(Canvas canvas, Size size) {
    _width = size.width;
    _height = size.height;

    _drawCanvas(canvas);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

draggable_canvas.dart

import 'package:flutter/material.dart';
import 'node.dart';
import 'draggable_painter.dart';

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

  @override
  State createState() => DraggableObjectPageState();
}

class DraggableObjectPageState extends State<DraggableObjectPage> {
  final List<Node> nodeList = List.empty(growable: true);

  var offsetX = 0.0;
  var offsetY = 0.0;

  var preX = 0.0;
  var preY = 0.0;

  Node currentNode;

  DraggableObjectPageState() {
    initNodes();
  }

  void initNodes() {
    nodeList.clear();
    nodeList.add(Node("1", "Node\n1", 150.0, 180.0));
    nodeList.add(Node("2", "Node\n2", 220.0, 40.0));
    nodeList.add(Node("3", "Node\n3", 380.0, 240.0));
    nodeList.add(Node("4", "Node\n4", 640.0, 190.0));
    nodeList.add(Node("5", "Node\n5", 480.0, 350.0));
  }

  Node getNode(x, y) {
    const radius = 30.0;
    for (final node in nodeList) {
      final c = Offset(node.x - x + offsetX, node.y - y + offsetY);
      if (c.distance <= radius) {
        return node;
      }
    }
    return null;
  }

  void _handlePanDown(details) {
    final x = details.localPosition.dx;
    final y = details.localPosition.dy;

    final node = getNode(x, y);
    if (node != null) {
      currentNode = node;
    } else {
      currentNode = null;
    }

    preX = x;
    preY = y;
  }

  void _handlePanUpdate(details) {
    final dx = details.localPosition.dx - preX;
    final dy = details.localPosition.dy - preY;

    if (currentNode != null) {
      setState(() {
        currentNode.x = currentNode.x + dx;
        currentNode.y = currentNode.y + dy;
      });
    } else {
      setState(() {
        offsetX = offsetX + dx;
        offsetY = offsetY + dy;
      });
    }

    preX = details.localPosition.dx;
    preY = details.localPosition.dy;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onPanDown: _handlePanDown,
        onPanUpdate: _handlePanUpdate,
        child: CustomPaint(
          child: Container(),
          painter: DraggablePainter(nodeList, offsetX, offsetY),
        ),
      ),
    );
  }
}