Things I learned while developing Flutter GraphView widget with MultiChildRenderObject

Nabil Mosharraf Hossain
4 min readAug 17, 2020

--

We say, In Flutter, everything is a widget. The Widget tree is a structure that represents how our widgets are organized. So I wanted to display my widgets in a tree layout and alas! I dont see any TreeView in flutter. So I took the daunting task to build one of my own in pub.dev

My initial help came from article by creativecreatorormaybenot, who explained how he created a clock using MultiChildRenderObject for flutter clock challange.

So I set out to think how can we make this view. I initially created using Rows and Columns. But definitely it was not good at performance. So the next question was:

Why not Custom Painter?

Yes, we could have done that, but it seemed to me that the layouting of the widgets would be much as flexible and less performant. So I opted for using the MutliChildRenderObject which lays out its children itself as well we can use custom painting on the canvas.

So here are the things I learnt:

1. We pass our children in the constructor

In MultiChildRenderObject, when we the constructor is setup we have to pass the list of Widget that will be laid out. No other manipulation on the children widgets can be easily done after this.

GraphView({Key key, @required this.graph, @required this.algorithm, EdgeRenderer renderer})
: assert(graph != null),
assert(algorithm != null),
renderer = renderer ?? ArrowEdgeRenderer(),
super(key: key, children: _extractChildren(graph));

2. Laying out: Constraints go down. Sizes go up. Parent sets position

The layout process in flutter is very simple, best described in this article

The MultiChildRenderObject gets its constraints from its parent, by Constraints, it means how much space this widget and its children can have.

So the main magic is the performLayout().

You have to ensure that you layout the child, which renders itself and inform our RenderObject. We then get the max Size of our Widget using our custom function algorithm.run() and then finally we lay down the childs to their specific coordinates using the child.offset parameter.

@override
void performLayout() {
if (childCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
}

RenderBox child = firstChild;
int position = 0;
while (child != null) {
final NodeBoxData node = child.parentData as NodeBoxData;

child.layout(BoxConstraints.loose(constraints.biggest), parentUsesSize: true);
graph.getNodeAtPosition(position).size = child.size;

child = node.nextSibling;
position++;
}

size = algorithm.run(graph, 10, 10);

child = firstChild;
position = 0;
while (child != null) {
final NodeBoxData node = child.parentData as NodeBoxData;

node.offset = graph.getNodeAtPosition(position).position;

child = node.nextSibling;
position++;
}
}
Tree Layout in Flutter GraphView

3. MarkNeedsLayout()

If there is any paramater that you are passing along to the render object, then we can tell our render object that our widget might need to update the layout completely or update only the paint method.

We need to pass the params to the overriden updateRenderObject class.

@override
RenderCustomLayoutBox createRenderObject(BuildContext context) {
return RenderCustomLayoutBox(graph, algorithm, paint);
}

@override
void updateRenderObject(BuildContext context, RenderCustomLayoutBox renderObject) {
renderObject
..graph = graph
..algorithm = algorithm
..customPaint = paint;
}

And then in the rendex widget box we ensure that the values that we set also calls markNeedsLayout().

Graph get graph => _graph;

set graph(Graph value) {
_graph = value;
markNeedsLayout();
}

4. Ensure that you offset the canvas

So after succesfully putting the nodes to their correct coordinates, it was time to render the arrows/edges. And for some reason it was wrong. The childs were painted by the function defaultPaint(context, offset);

So we had to actually translate our canvas according to the offset given by our parent in the overriden paint() class.

@override
void paint(PaintingContext context, Offset offset) {
var paint = Paint()
..color = Colors.black
..strokeWidth = 3
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.butt;

context.canvas.save();
context.canvas.translate(offset.dx, offset.dy);


_renderer.render(context.canvas, graph, paint);

context.canvas.restore();

defaultPaint(context, offset);
}
Force Directed Graph in Flutter GraphView

5. Ensure hitTestChidren() is overridden

The final thing I learnt was actually after releasing the inital version of my library. One of the users of my library contacted me via email and said that the Widgets onTap does not work whether is GestureListener or Inkwell. After researching I learnt that I had to override the hitTestChildren() in the RenderWidget as well.

@override
bool hitTestChildren(BoxHitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}

Thats All.

I learnt a lot and it feels great that I was able to contribute to a community that I loved to be part of. Hope you enjoy my graphview library.

Checkout the github: https://github.com/nabil6391/graphview

Checkout in pub.dev https://pub.dev/packages/graphview

However I still have a couple of things to learn, hopefully some day in the future:

  1. RepaintBoundary
  2. Should I use sliver?
  3. Animations inside MultiChildRenderObject

--

--