I need to render C4model diagrams in my Flutter app for my project. I receive data as text and need to transform that into the diagram and render it, but the items need to be pressable.
Now I could try to add the lines, boxes, text by my own with available widgets in Flutter and do the styling and position myself to match with the C4Model. But I am not really advanced in Flutter, and I also don't really know how I would do it manually. It seems pretty hard and I started just one month ago with Flutter. I first tried looking for a simpler solution so I found that there is a package net.sourceforge.plantuml
that I can use in a Spring Boot java controller and return an SVG based on text. Setting up the spring backend was easy and straightforward.
I also found out there is a package in Flutter that can render SVG's, the flutter_svg
package. Installing that and using it with the SVG received from the Backend/API was also pretty easy and quickly done using SvgPicture.network("http://localhost:8082/")
or SvgPicture.asset("assets/c4.svg")
,
Now there is one more challenge and the hardest challenge is, I need all the different elements in the SVG to be clickable so I can navigate by clicking on the different items.
It was hard finding a solution for this. I found this question: Draw and interact with SVG in Flutter. But here the svg exists of paths and my generated svg does not contain paths. Also here it is a static svg but I need to generate SVG's in runtime based on the text, so this did not solve my problem. Here is my SVG:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentstyletype="text/css" height="337px" preserveAspectRatio="none" style="width:423px;height:337px;background:#FFFFFF;" version="1.1" viewBox="0 0 423 337" width="423px" zoomAndPan="magnify"><defs></defs><!--MD5=[b0dcb10635bb063d35b19b99258b98bc]
entity personAlias--><g id="elem_personAlias" data-name="Persoon 1"><rect fill="#08427B" height="133.9531" rx="2.5" ry="2.5" style="stroke:#073B6F;stroke-width:0.5;" width="165" x="207" y="7"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="52" x="263.5" y="28.6016">«person»</text><image height="48" width="48" x="265.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII=" y="31.1328"></image><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="268" y="94.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="287.5" y="111.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="221" y="128">Optional Description</text></g><!--MD5=[17b12dafac73cdd4ee2c65b8eac9984a]
entity containerAlias--><g id="elem_containerAlias"><rect fill="#438DD5" height="100.0859" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="165" x="207" y="231"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="65" x="257" y="252.6016">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="268" y="270.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="73" x="253" y="285.5781">[Technology]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="287.5" y="301.6445"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="221" y="318.1328">Optional Description</text></g><!--MD5=[f2180e1b77c2df9e82dee62ba9111ff0]
entity systemAlias--><g id="elem_systemAlias"><rect fill="#1168BD" height="85.9531" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="165" x="7" y="238"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="52" x="63.5" y="259.6016">«system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="68" y="277.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="87.5" y="294.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="21" y="311">Optional Description</text></g><!--MD5=[b13543e63a122c5668c8d1fc2febc05a]
entity extSystemAlias--><g id="elem_extSystemAlias"><rect fill="#999999" height="85.9531" rx="2.5" ry="2.5" style="stroke:#8A8A8A;stroke-width:0.5;" width="165" x="7" y="31"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="105" x="37" y="52.6016">«external_system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="68" y="70.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="87.5" y="87.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="21" y="104">Optional Description</text></g><!--MD5=[533f264152b6108a8baf1e8f44cb7b13]
link personAlias to containerAlias--><g id="link_personAlias_containerAlias"><path d="M289.5,141.27 C289.5,167 289.5,196.22 289.5,221.18 " fill="none" id="personAlias-to-containerAlias" style="stroke:#666666;stroke-width:1.0;"></path><polygon fill="#666666" points="289.5,229.01,292.5,221.01,286.5,221.01,289.5,229.01" style="stroke:#666666;stroke-width:1.0;"></polygon><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="33" x="336.5" y="183.6016">Label</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="125" x="290.5" y="197.7344">[Optional Technology]</text></g><!--MD5=[40c7045c902962189cbb70a245a6c67f]
reverse link extSystemAlias to systemAlias--><g id="link_extSystemAlias_systemAlias"><path d="M89.5,126.85 C89.5,161.1 89.5,205.43 89.5,237.58 " fill="none" id="extSystemAlias-backto-systemAlias" style="stroke:#666666;stroke-width:1.0;"></path><polygon fill="#666666" points="89.5,118.99,86.5,126.99,92.5,126.99,89.5,118.99" style="stroke:#666666;stroke-width:1.0;"></polygon><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="33" x="136.5" y="183.6016">Label</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="125" x="90.5" y="197.7344">[Optional Technology]</text></g></svg>
You can see each <g>
element has a well defined id already generated by PlantUML, my first thought was I can simply add a onTap listener for each id, like html and javascript but that was not possible.
Next I tried to render each individual element separately and have a GestureDetector for each element. That worked out but the position got lost because I used a Column so each element was rendered in a new row.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:xml/xml.dart';
import 'package:path_parsing/path_parsing.dart';
import 'package:flutter/services.dart';
const svgString = '''
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentstyletype="text/css" height="337px" preserveAspectRatio="none" style="width:423px;height:337px;background:#FFFFFF;" version="1.1" viewBox="0 0 423 337" width="423px" zoomAndPan="magnify"><defs></defs><!--MD5=[b0dcb10635bb063d35b19b99258b98bc]
entity personAlias--><g id="elem_personAlias"><rect fill="#08427B" height="133.9531" rx="2.5" ry="2.5" style="stroke:#073B6F;stroke-width:0.5;" width="165" x="207" y="7"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="52" x="263.5" y="28.6016">«person»</text><image height="48" width="48" x="265.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII=" y="31.1328"></image><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="268" y="94.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="287.5" y="111.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="221" y="128">Optional Description</text></g><!--MD5=[17b12dafac73cdd4ee2c65b8eac9984a]
entity containerAlias--><g id="elem_containerAlias"><rect fill="#438DD5" height="100.0859" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="165" x="207" y="231"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="65" x="257" y="252.6016">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="268" y="270.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="73" x="253" y="285.5781">[Technology]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="287.5" y="301.6445"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="221" y="318.1328">Optional Description</text></g><!--MD5=[f2180e1b77c2df9e82dee62ba9111ff0]
entity systemAlias--><g id="elem_systemAlias"><rect fill="#1168BD" height="85.9531" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="165" x="7" y="238"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="52" x="63.5" y="259.6016">«system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="68" y="277.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="87.5" y="294.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="21" y="311">Optional Description</text></g><!--MD5=[b13543e63a122c5668c8d1fc2febc05a]
entity extSystemAlias--><g id="elem_extSystemAlias"><rect fill="#999999" height="85.9531" rx="2.5" ry="2.5" style="stroke:#8A8A8A;stroke-width:0.5;" width="165" x="7" y="31"></rect><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="105" x="37" y="52.6016">«external_system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="43" x="68" y="70.6016">Label</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="87.5" y="87.5117"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141" x="21" y="104">Optional Description</text></g><!--MD5=[533f264152b6108a8baf1e8f44cb7b13]
link personAlias to containerAlias--><g id="link_personAlias_containerAlias"><path d="M289.5,141.27 C289.5,167 289.5,196.22 289.5,221.18 " fill="none" id="personAlias-to-containerAlias" style="stroke:#666666;stroke-width:1.0;"></path><polygon fill="#666666" points="289.5,229.01,292.5,221.01,286.5,221.01,289.5,229.01" style="stroke:#666666;stroke-width:1.0;"></polygon><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="33" x="336.5" y="183.6016">Label</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="125" x="290.5" y="197.7344">[Optional Technology]</text></g><!--MD5=[40c7045c902962189cbb70a245a6c67f]
reverse link extSystemAlias to systemAlias--><g id="link_extSystemAlias_systemAlias"><path d="M89.5,126.85 C89.5,161.1 89.5,205.43 89.5,237.58 " fill="none" id="extSystemAlias-backto-systemAlias" style="stroke:#666666;stroke-width:1.0;"></path><polygon fill="#666666" points="89.5,118.99,86.5,126.99,92.5,126.99,89.5,118.99" style="stroke:#666666;stroke-width:1.0;"></polygon><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="33" x="136.5" y="183.6016">Label</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="125" x="90.5" y="197.7344">[Optional Technology]</text></g></svg>
''';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
/// Parses Svg from provided asset path
List<XmlElement> loadFromFile() {
// String svgString = await rootBundle.loadString(file);
var svg2 = XmlDocument.parse(svgString);
var svg3 = svg2.findAllElements('g');
return svg3.toList();
}
List<Widget> getList() {
List<Widget> childs = loadFromFile().map(
(e) {
return Expanded(
child: GestureDetector(
onTap: () {
e.attributes.forEach(
(attribute) {
print(attribute);
},
);
},
child: SvgPicture.string('''
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentstyletype="text/css" height="337px" preserveAspectRatio="none" style="width:423px;height:337px;background:#FFFFFF;" version="1.1" viewBox="0 0 423 337" width="423px" zoomAndPan="magnify"><defs></defs><!--MD5=[b0dcb10635bb063d35b19b99258b98bc]
entity personAlias-->
${e.toString()}
</svg>
'''),
behavior: HitTestBehavior.translucent,
),
);
},
).toList();
return childs;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("widget.title"),
),
body: Center(
child: Column(
children: [...getList()],
),
),
);
}
}

I tried to solve this by using a Stack instead of a Column and removing the Expanded widget but this did not fully work out because the Gesturedetector take the entire space and not just the box that is rendered. And the item that is rendered as last will be on top an only that click listener is available.
This is also the place where I am stuck now.
I wondered if there is a way that I can let the GestureDetector be listening to the onTap where there is something rendered and not in the whitespaces. But besides my approach there might be better solutions on how I could solve this problem and I would like to hear all possibilities.