项目数组作为 StatefulWidget 中的状态并确保“setState”仅触发数组中已更改项目的更新?

问题描述

背景 - 希望为 StatefulWidget 使用动态项目列表。在我的用例中,小部件将调用 CustomePainter(画布),因此有时会在画布上绘制不同数量的图像,因此在父 StatefulWidget 中希望有一个“图像数组”。

问题 - 如果使用数组作为状态变量,我需要以编程方式做什么(如果有的话)以确保只有数组中已更改的项目实际上会“重绘”,在特别是在这种情况下在画布上“重新绘制”。

(也许这里有两个单独的答案,一个针对具有 (a) 标准小部件的数组,另一个针对将 (b) 项传递给 CustomePainter 以在画布上绘画的情况??)

作为示例,请参见下面的代码(@ch271828n 提供此代码是为了帮助我解决一个单独的问题 - Getting 'Future<Image>' can't be assigned to a variable of type 'Image' in this Flutter/Dart code?)。这段代码突出了主要思想,但不包括作为参数传递给 CustomPainter。

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'),),);
  }
}

解决方法

首先,谈谈build:确实您需要一个状态管理解决方案。也许看看https://flutter.dev/docs/development/data-and-backend/state-mgmt/options。我个人建议使用 MobX,它只需要很少的样板文件并且可以使开发速度更快。

使用 setState 不是一个好主意。 setState 在查看源代码时,除了:

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

所以它只是 markNeedsBuild - 整个有状态小部件再次被称为 build。您传递给 setState 的回调没有什么特别之处。 因此这种方式不能触发部分重建。

其次,您只想减少重绘,而不是减少构建次数,因为 Flutter 的设计使得 build 可以轻松地以 60fps 的速度调用。然后事情就变得简单了:

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;
  }
}

请注意,您可以随时在主页中调用 setState,因为这样很便宜。 (如果您的主页有很多子小部件,那么可能不好,那么您需要状态管理解决方案)。但是除非shouldRepaint 这么说,否则画家不会重新绘制。是啊!

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...