How does CustomPainter.shouldRepaint() work in Flutter?

1.9k Views Asked by At

In Flutter, when creating a CustomPainter, there is an override method, shouldRepaint() that you can return either true or false... presumably to tell the system whether or not to repaint the view.

And in the docs, the description for the method is:

shouldRepaint(covariant CustomPainter oldDelegate) → bool Called whenever a new instance of the custom painter delegate class is provided to the RenderCustomPaint object, or any time that a new CustomPaint object is created with a new instance of the custom painter delegate class (which amounts to the same thing, because the latter is implemented in terms of the former). [...]

I basically don't understand any of that other than the fact that it returns a bool. That makes my head hurt! I also suspect that delving deeper into the definition of "custom painter delegate class," or "RenderCustomPaint object," will not be an enlightening experience.

I'm confused because:

  • I thought we didn't have to worry about when a widget "should repaint" because Flutter was supposed to decide where and when to re-render the widget tree based on it's own complex optimization decisions.

  • I thought the paint() method was where you define "this is how this view paints itself, (always and whenever that is necessary)"

  • All the examples I have found simply return false from this method... but I have noticed different behavior when using true vs false.

  • If we are always returning false, then how does it ever repaint? (And it does repaint even when false)

  • If the only possible logic available to us is comparing the "oldDelegate" to (something?) then why are we required to override the method at all?

  • I haven't seen any example that demonstrates why or how you would return TRUE, and what the logic of such an example would look like in order to make that decision.

Why and how would a knowledgable person decide to return false?

Why and how would a knowledgable person decide to return true?

Can anyone explain it like you're talking to a 13 year old (not Linus Torvalds)?

A simple code example and counter-example would be great (as opposed to an exhaustive explicit explanation!)

2

There are 2 best solutions below

0
On BEST ANSWER

I have used CustomPainter extensively, and here is my answer.

Firstly, here is the full doc. You may have only read the starting sentences instead of the full doc. https://api.flutter.dev/flutter/rendering/CustomPainter/shouldRepaint.html

Why and how would a knowledgeable person decide to return false/true?

Here is the rule: If the new instance represents different information than the old instance, then the method should return true, otherwise it should return false.

Example:

class MyPainter extends CustomPainter {
  MyPainter() : super();

  @override
  void paint(Canvas canvas, Size size) => canvas.drawRect(Offset.zero & size, Paint());

  // false since all instances of MyPainter contain same information
  @override
  bool shouldRepaint(MyPainter oldDelegate) => false;
}

class MyPainter extends CustomPainter {
  final Color color;
  final double width;

  MyPainter(this.color, this.width) : super();

  @override
  void paint(Canvas canvas, Size size) => canvas.drawRect(
      Offset.zero & size,
      Paint()
        ..color = color
        ..strokeWidth = width);

  @override
  bool shouldRepaint(MyPainter oldDelegate) => oldDelegate.color != this.color || oldDelegate.width != this.width;
}

I thought we didn't have to worry about when a widget "should repaint" because Flutter was supposed to decide where and when to re-render the widget tree based on it's own complex optimization decisions.

Yes and no. This shouldRepaint() is basically an optimization for speed. You can return constantly true if you do not care about performance.

I thought the paint() method was where you define "this is how this view paints itself, (always and whenever that is necessary)"

"this is how this view paints itself" - yes. "always and whenever that is necessary" - partially no. If you provide wrong information to shouldRepaint() you may miss some paints.

All the examples I have found simply return false from this method... but I have noticed different behavior when using true vs false.

What??? I see people returning true, or returning with comparison (see my example below). But when returning false, it can cause problems. Even simply look at comments of this function you will see it should cause problems with constant false. But anyway, if your painter really does not contain any information that can change, it is ok...

If we are always returning false, then how does it ever repaint? (And it does repaint even when false)

  /// If the method returns false, then the [paint] call might be optimized
  /// away.
  ///
  /// 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).

Notice the "might", and the paragraph below. Flutter can choose to paint or not to paint.

If the only possible logic available to us is comparing the "oldDelegate" to (something?) then why are we required to override the method at all?

See my example

I haven't seen any example that demonstrates why or how you would return TRUE, and what the logic of such an example would look like in order to make that decision.

See my example


By the way, it would be great if you gain some insights of Flutter, such as the widget/layout/paint logic etc, then you will easily understand the problem. But anyway I have answered above using words that are hopefully easy to understand even without deep understanding of Flutter.

0
On

It means always repaint if setState() method is called from the State class The widgets in the State class will be updated if setState() changes their data.

shouldRepaint() EXAMPLE:

check at the bottom for inline comments!!

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true; 
    // true means Always repaint if this CustomPainter is updated anywhere else
             // e.g. from the State class 
             // State class can be called as below 
}

Complete code ...

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

void main() {
    runApp(const MyApp());
}

class MyApp extends StatelessWidget {  
    const MyApp({super.key});
      // This widget is the root of your application.

    @override
    Widget build(BuildContext context) {
        return MaterialApp(
                  title: 'Flutter Custom Paint example ',
                  //theme: ThemeData(  //use theme if needed
                  //     brightness: Brightness.dark,
                  //     primaryColor: Colors.indigo,
                  //     accentColor: Colors.indigoAccent
                  //    ),      
                  home: _CustomPainterView()  // _is implemented in this file
                              //first return the StatefulWidget                      
                        
               );
    }
}


class _CustomPainterView extends StatefulWidget{ //must be a stateful widget
    @override
    State<StatefulWidget> createState() {
        return _CustomViewState();   //here return the State widget
    }
}


//State class below references the Stateful widget from which it was called...

class _CustomViewState extends State<_CustomPainterView>{
    Map<String, Object> painterData = {'screenMessage': 'not yet swiped', 'var_1': 0, 'var_2': 0.0  };
//all the needed parameters for the instance of  CustomPainter class must be defined here in the State Class. 
//they will be passed by setState() method to the  CustomPainter which will provides the actual canvas

@override
Widget build(BuildContext context) {
    return  Scaffold(             
          appBar: AppBar( title: Text("Custom Painter Example") ) ,
            body: Container(
                    width: MediaQuery.of(context).size.width,//get width from device
                    height: MediaQuery.of(context).size.width*2, //i use the the custom width from half of the height                                                                                      
                    // ignore: sort_child_properties_last
                    child:  GestureDetector( //to allow screen swipe or drag    
                                child: CustomPaint(//CustomPaint() is just a container for actual painter. 
                                                   //note the spelling 
                                        painter: _CustomPainterExample(painterData) 
                                                  //return CustomPainter() 
                                                  //supply constructor data
                                       ),  
                                onVerticalDragUpdate: (details) {
                                  int sensitivity = 1;// = 1 every pixel swiped will be detected by below code
                                  setState( (){                                          
                                      if  (details.delta.dy > sensitivity) {                                              
                                            debugPrint("swipe down");   //print to the debug console                                           
                                            painterData['screenMessage'] = "swipe down";
                                            //this change only the key-value that needs to be changed in the key-value pairs, then repaint.
                                            // painterData map will change but inside setState() will cause other effect                                                
                                            //setState  recalls CustomPainter consctructor every time
                                            //setState force a repaint in  CustomPainter
                                      } 
                                      if(details.delta.dy < -sensitivity){                                           
                                          debugPrint("swipe up");  
                                            painterData['screenMessage'] = "swipe up";                               
                                      }
                                                                         
                                    }
                                  );
                                },
                                onHorizontalDragUpdate: (details) {
                                  int sensitivity = 1;
                                  setState( (){                                           
                                       if  (details.delta.dx > sensitivity) {                                             
                                            debugPrint("swipe right");
                                              painterData['screenMessage'] = "swipe right";
                                      } 
                                      if(details.delta.dx < -sensitivity){
                                          
                                          debugPrint("swipe left");  
                                          painterData['screenMessage'] = "swipe left";                             
                                      }                                    
                                    }
                                  );
                                },
                                   
                            ),  
                            //color: Color.fromARGB(255, 50, 57, 126)
                    // ignore: prefer_const_constructors
                    color: Color.fromARGB(255, 1, 108, 141),
                    
                  ),
        );
    }
}

class _CustomPainterExample extends CustomPainter {
    Map<String, Object> painterData = new Map<String, Object>();

    _CustomPainterExample(Map<String, Object> pdata){
        painterData = pdata;
      }

    @override
    void paint(Canvas canvas, Size size) {
        var centerX = size.width/2;
        var centerY = size.height/2;
        var center = Offset(centerX,centerY);
        var radius = min(centerX,centerY);
        var fillBrush = Paint()
        // ignore: prefer_const_constructors
        ..color = Color.fromARGB(255, 202, 122, 29);
        canvas.drawCircle( center, radius, fillBrush );
        //can also draw using the data from constructor method
  
        var textPainter = TextPainter(   
                    text: TextSpan(
                           text:  painterData['screenMessage'].toString(),
                           style: TextStyle(color: Color.fromARGB(255, 245, 242, 242),fontSize: 30,),
                          ),
                    textDirection: TextDirection.ltr,                        
                  );
      textPainter.layout(minWidth: 0, maxWidth: size.width);//<<< needed method
      textPainter.paint(canvas, Offset(5.0, (90/100)*size.height));
  } //customPainter

      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;  //always repaint if setState() is called from the State  class. Look for setState() method in the class: _CustomViewState 
        //It will update canvas since the _CustomPainterExample is one of widgets in the _CustomViewState which is the State class. All widgets in the State class will be updated if SetState() changes their data.
      }

}