Background - want to utilise a dynamic list of items for a StatefulWidget. In my usecase the widget will be calling a CustomePainter (canvas) so sometimes there will be a varying number of images to be drawn on the canvas, hence within parent StatefulWidget would like to have an "array of images".

Question - if using an array as the state variable what do I need to do programmtically (if anything) to ensure only the items that have changed within the array do infact get "redrawn", in particular in this case get "re-painted" on the canvas.

(Perhaps there are two separate answers here one for the array having (a) standard widgets, and one for the case where (b) items are being passed to a CustomePainter for painting on a canvas??)

As an example see code below (@ch271828n provided this to assist me here on a separate question - Getting 'Future<Image>' can't be assigned to a variable of type 'Image' in this Flutter/Dart code?). This code highlights the main idea, but doesn't include the passing onto a CustomPainter as parameters.

import 'dart:typed_data';
import 'dart:ui' as ui;

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<ui.Image> _backgroundImages;

  @override
  void initState() {
    super.initState();
    _asyncInit();
  }

  Future<void> _asyncInit() async {
    final imageNames = ['firstimage', 'secondimage', 'thirdimage'];
    // NOTE by doing this, your images are loaded **simutanously** instead of **waiting for one to finish before starting the next**
    final futures = [for (final name in imageNames) loadImage(name)];
    final images = await Future.wait(futures);
    setState(() {
      _backgroundImages = images;
    });
  }

  Future<ui.Image> loadImage(imageString) async {
    ByteData bd = await rootBundle.load(imageString);
    // ByteData bd = await rootBundle.load("graphics/bar-1920×1080.jpg");
    final Uint8List bytes = Uint8List.view(bd.buffer);
    final ui.Codec codec = await ui.instantiateImageCodec(bytes);
    final ui.Image image = (await codec.getNextFrame()).image;
    return image;
    // setState(() => imageStateVarible = image);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: _backgroundImages != null ? YourWidget(_backgroundImages) : Text('you are loading that image'),
      ),
    );
  }
}


Solution 1: ch271828n

Firstly, talking about builds: Indeed you need a state management solution. Maybe look at https://flutter.dev/docs/development/data-and-backend/state-mgmt/options. Personally I suggest MobX which requires few boilerplate and can make development much faster.

Using setState is not a good idea. The setState, when looking into source code, does nothing but:

  void setState(VoidCallback fn) {
    assert(...);
    _element.markNeedsBuild();
  }

So it is nothing but markNeedsBuild - the whole stateful widget is called build again. Your callback passed into setState has nothing special. Thus this way cannot trigger partial rebuild.

Secondly, you only want to reduce repaint, but not reduce number of builds, because flutter is designed such that build can be called by 60fps easily. Then things become easy:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        for (final image in _yourImages)
          CustomPaint(
            painter: _MyPainter(image),
          ),
      ],
    );
  }
}

class _MyPainter extends CustomPainter {
  final ui.Image image;

  _MyPainter(this.image);

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // paint it
  }

  @override
  bool shouldRepaint(covariant _MyPainter oldDelegate) {
    return oldDelegate.image != this.image;
  }
}

Notice that, you can call setState in homepage whenever you like, because that is cheap. (if your homepage has a lot of children widget then maybe not good, then you need state management solution). But the painter will not repaint unless shouldRepaint says so. Yeah!