2

Minimal reproducible code:

void main() => runApp(MaterialApp(home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Offset> _points = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {}), // This setState works
        child: Icon(Icons.refresh),
      ),
      body: GestureDetector(
        onPanUpdate: (details) => setState(() => _points.add(details.localPosition)), // but this doesn't...
        child: CustomPaint(
          painter: MyCustomPainter(_points),
          size: Size.infinite,
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  final List<Offset> points;
  MyCustomPainter(this.points);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.red;
    for (var i = 0; i < points.length; i++) {
      if (i + 1 < points.length) {
        final p1 = points[i];
        final p2 = points[i + 1];
        canvas.drawLine(p1, p2, paint);
      }
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) => false;
}

Try to draw something by long dragging on the screen, you won't see anything drawn. Now, press the FAB which will reveal the drawn painting maybe because FAB calls setState but onPanUpdate also calls setState and that call doesn't paint anything on the screen. Why?

Note: I'm not looking for a solution on how to enable the paint, a simple return true does the job. What I need to know is why one setState works (paints on the screen) but the other fails.

iDecode
  • 22,623
  • 19
  • 99
  • 186

1 Answers1

1

To understand why setState() in onPanUpdate is not working you might want to look into the widget paint Renderer i.e., CustomPaint.

The CustomPaint (As stated by docs as well) access the painter object (in your case MyCustomPainter) after finishing up the rendering of that frame. To confirm we can check the source of CustomPainter. we can see markNeedsPaint() is called only while we are accessing painter object through setter. For more clarity you might want to look into source of RenderCustomPaint , you will definitely understand it :

void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
    // Check if we need to repaint.
    if (newPainter == null) {
      assert(oldPainter != null); // We should be called only for changes.
      markNeedsPaint();
    } else if (oldPainter == null ||
        newPainter.runtimeType != oldPainter.runtimeType ||
        newPainter.shouldRepaint(oldPainter)) { //THIS
      markNeedsPaint();
    }

    .
    .
    .
}

While on every setState call your points are updating but every time creating new instances of 'MyCustomPainter` is created and the widget tree is already rendered but painter have not yet painted due to reason mentioned above.

That is why the only way to call markNeedPaint()(i.e., to paint your object), is by returning true to shouldRepaint or Either oldDeleagate is null which only happens and Fist UI build of the CustomPainter, you can verify this providing some default points in the list.

It is also stated that

It's possible that the [paint] method will get called even if [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to be repainted). It's also possible that the [paint] method will get called without [shouldRepaint] being called at all (e.g. if the box changes size).

So the only reason of setState of Fab to be working here (which seams valid) is that Fab is somehow rebuilding the any parent of the custom painter. You can also try to resize the UI in 'web build' or using dartpad you will find that as parent rebuilds itself the points will become visible So setState directly have nothing to do with shouldRepaint. Even hovering on the fab (in dartpad) button will cause the ui to rebuild and hence points will be visible.

Sahdeep Singh
  • 1,342
  • 1
  • 13
  • 34
  • Hi, thanks for your answer, I knew the part you mentioned for `returning false`, but the main question was why FAB's `setState` works, for which you replied "Fab is somehow rebuilding the any parent of the custom painter", I still didn't get it. – iDecode May 05 '21 at 09:46
  • @iDecode One weird finding. If you access _points anywhere else like this `appBar: AppBar( title: Text(_points.length.toString()), ),` your code will work :0 – Sahdeep Singh May 05 '21 at 12:48
  • I also didn't get that how `RenderCustomPaint` is coming into play. I'm using `CustomPainter` class and that class itself never used `RenderCustomPaint` object in it. So, how `_didUpdatePainter` is getting called. – iDecode May 06 '21 at 10:23
  • It is being used. You can see a overridden method in `CustomPaint` called `createRenderObject` – Sahdeep Singh May 06 '21 at 13:13
  • I was thinking it should be in the `CustomPainter` class but it was in `CustomPaint`. Thanks – iDecode May 08 '21 at 05:38