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.
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 seemarkNeedsPaint()
is called only while we are accessingpainter
object through setter. For more clarity you might want to look into source ofRenderCustomPaint
, you will definitely understand it :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 returningtrue
toshouldRepaint
or EitheroldDeleagate
is null which only happens and Fist UI build of theCustomPainter
, you can verify this providing some default points in the list.It is also stated that
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.