You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中Stepper内CustomPainter引发无限循环与索引越界问题

Flutter Stepper结合CustomPainter时的无限重绘与索引越界问题

在Flutter的Stepper组件中使用CustomPainter时遇到以下问题:

  • 页面加载后CustomPainter持续重绘,触发无限循环
  • 抛出「RangeError (index): Index out of range: no indices are valid: 0」错误

包含Stepper的页面逻辑代码

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int currentStep = 0;
  List<LayoutBuilderWidget> listDynamic = [];

  addDynamic() {
    numberofwalls ++;
    listDynamic.add(LayoutBuilderWidget(numberofwalls));
    LengthFT = [];
    LengthIN = [];
    Angle = [];
    setState(() {});
  }

  submitdata(){
    double MathLengthFT;
    double finalLength;
    LengthFT = [];
    LengthIN = [];
    Angle = [];
    listDynamic.forEach((widget) => LengthFT.add(widget.lengthFT.text));
    listDynamic.forEach((widget) => LengthIN.add(widget.lengthIN.text));
    listDynamic.forEach((widget) => Angle.add(widget.dropdownValue));

    for(int i=0; i < numberofwalls; i++)
    {
      if(LengthIN[i] == null)
        {
          double MathLengthFT = int.parse(LengthFT[i]) * 12;
          FinalLength.add(MathLengthFT.toString());
        }
        if(LengthFT[i] == null)
        {
          finalLength = double.parse(LengthIN[i]);
          FinalLength.add(finalLength.toString());
        } 
        MathLengthFT = int.parse(LengthFT[i]) * 12;
        finalLength = int.parse(LengthIN[i]) + MathLengthFT;
        FinalLength.add(finalLength.toString());
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:Stepper(
        type: StepperType.horizontal,
        steps: getSteps(),
        currentStep:  currentStep,
        onStepContinue: () {
          final isLastStep = currentStep == getSteps().length - 1;
          if(isLastStep){
            print('Complete');
          }
          else{
            setState(() {
              currentStep += 1;
            });
          }
        },
        onStepCancel: currentStep == 0 ? null : () => setState(() => currentStep -= 1),
      ), 
    );
  }

  List<Step> getSteps() => [
    Step(
      isActive: currentStep >= 0,
      title: const Text('Layout'), 
      content: Container(
        child: Row(
          children: [
            Container(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Container(
                        alignment: Alignment.center,
                        margin: const EdgeInsets.all(20),
                        width: 200,
                        child: Text(
                          'Wall',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      ),
                      Container(
                        alignment: Alignment.center,
                        margin: const EdgeInsets.all(20),
                        width: 200,
                        child: Text(
                          'Length', 
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      ),
                      Container(
                        alignment: Alignment.center,
                        margin: const EdgeInsets.all(20),
                        width: 200,
                        child: Text(
                          'Angle',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      ),
                    ],
                  ),
                  Container(
                    height: 600,
                    width: 1000,
                    child: ListView.builder(
                      itemCount: listDynamic.length,
                      itemBuilder: ((_, index) => listDynamic[index])
                    )
                  ),
                  FloatingActionButton.extended(onPressed: () => addDynamic(), label: const Text('Add Wall'), icon: const Icon(Icons.add),),
                  FloatingActionButton.extended(onPressed: () {submitdata();}, icon: const Icon(Icons.visibility), label: const Text(''),),
                ],
              ),
            ),
            Column(
              children: [
                Container(
                  color: Color.fromARGB(255, 241, 241, 241),
                  width: 450,
                  height: 450,
                  child: CustomPaint(
                    foregroundPainter: OpenPainter(),
                  ),
                ),
                Container(
                  margin: EdgeInsets.only(top: 20),
                  width: 450,
                  height: 450,
                  child: CustomPaint(
                    painter: CanvasLegend(),
                  ),
                ),
              ],
            )
          ],
        ),
      ),
    ),
    Step(
      isActive: currentStep >= 1,
      title: const Text('Doors & Options'), 
      content: Container(),
    ),
    Step(
      isActive: currentStep >= 2,
      title: const Text('Panel Config'), 
      content: Container(),
    ),
    Step(
      isActive: currentStep >= 3,
      title: const Text('Accessories'), 
      content: Container(),
    ),
    Step(
      isActive: currentStep >= 4,
      title: const Text('Summary & Prices'), 
      content: Container(),
    ),
  ];
}

CustomPainter实现代码

class OpenPainter extends CustomPainter {
  OpenPainter();

  @override
  void paint(Canvas canvas, Size size) {
    var line1 = Paint()..strokeWidth = 5;
    var line2 = Paint()..strokeWidth = 5;
    double lastXPosition = 225;
    double lastYPosition = 225;
    double NextXPosition = 225;
    double NextYPosition = 225;
    int i = 0;
    
    if(numberofwalls != 0){
      for(i = 0; i < numberofwalls; i++)
      {
        NextYPosition = lastYPosition + double.parse(FinalLength[i]) * cos(AngleToRadians(double.parse(Angle[i])));
        NextXPosition = lastXPosition + double.parse(FinalLength[i]) * sin(AngleToRadians(double.parse(Angle[i])));

        canvas.drawLine(
          Offset(lastXPosition,lastYPosition),
          Offset(NextXPosition, NextYPosition),
          line1,
        );

        lastYPosition = NextYPosition;
        lastXPosition = NextXPosition;
      }
      i = 0;
    }
  }
 
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

double AngleToRadians(double angle){
  return (pi / 180) * angle;
}

问题原因与修复方案

1. 无限重绘问题

原因:OpenPainter的shouldRepaint直接返回true,每次页面刷新都会强制重绘;加上Stepper本身会因状态变化触发重建,形成无限循环。
修复:让OpenPainter接收绘制所需数据,仅在数据变化时重绘:

class OpenPainter extends CustomPainter {
  final int numberOfWalls;
  final List<String> finalLength;
  final List<String> angle;

  OpenPainter({required this.numberOfWalls, required this.finalLength, required this.angle});

  @override
  bool shouldRepaint(OpenPainter oldDelegate) {
    return oldDelegate.numberOfWalls != numberOfWalls ||
           oldDelegate.finalLength != finalLength ||
           oldDelegate.angle != angle;
  }

  // 其余paint方法逻辑不变
}

2. 索引越界错误

原因:

  • submitdata中未清空FinalLength就重复添加数据,导致长度不匹配;同时条件判断逻辑错误(重复执行添加代码)
  • OpenPainter依赖全局变量,数据更新不同步,导致绘制时索引超出数组长度
    修复:
  • 修正submitdata逻辑:
submitdata(){
  double mathLengthFT;
  double finalLength;
  LengthFT = [];
  LengthIN = [];
  Angle = [];
  FinalLength.clear(); // 先清空旧数据
  listDynamic.forEach((widget) => LengthFT.add(widget.lengthFT.text));
  listDynamic.forEach((widget) => LengthIN.add(widget.lengthIN.text));
  listDynamic.forEach((widget) => Angle.add(widget.dropdownValue));

  for(int i=0; i < numberofwalls; i++)
  {
    // 判断空字符串而非null(TextField的text默认是空字符串)
    if(LengthIN[i].isEmpty) {
      mathLengthFT = int.parse(LengthFT[i]) * 12;
      FinalLength.add(mathLengthFT.toString());
    }
    else if(LengthFT[i].isEmpty) {
      finalLength = double.parse(LengthIN[i]);
      FinalLength.add(finalLength.toString());
    } 
    else {
      mathLengthFT = int.parse(LengthFT[i]) * 12;
      finalLength = int.parse(LengthIN[i]) + mathLengthFT;
      FinalLength.add(finalLength.toString());
    }
  }
  setState(() {}); // 触发UI更新,同步数据到CustomPainter
}
  • 传递参数给OpenPainter:
CustomPaint(
  foregroundPainter: OpenPainter(
    numberOfWalls: numberofwalls,
    finalLength: FinalLength,
    angle: Angle,
  ),
),

3. 其他优化建议

  • 将全局变量(numberofwalls、LengthFT、LengthIN、Angle、FinalLength)移至_MyHomePageState类中作为成员变量,避免全局状态混乱
  • LayoutBuilderWidget中的输入框改用TextEditingController管理,确保状态同步正确

内容的提问来源于stack exchange,提问作者Aiden P

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 06:30:51