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

Flutter中‘Tried to paint a RenderObject reentrantly’错误排查与修复

问题:Flutter Bloc状态管理下拉框出现渲染递归错误

尝试通过API获取国家名称,使用Bloc库进行状态管理并在自定义下拉框组件中展示,运行后触发渲染递归错误。


相关代码

1. 自定义下拉框组件 select_input_field.dart

import 'package:flutter/material.dart';

class SelectInputField extends StatelessWidget {
  final String label;
  final List<String> options;
  final TextEditingController controller;
  final EdgeInsets padding;

  const SelectInputField(this.label, this.options,
      { Key? key,
        required this.controller,
        this.padding =
          const EdgeInsets.only(top: 8, bottom: 8, left: 30, right: 30)})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: padding,
      child: DropdownButtonFormField(
        decoration: InputDecoration(
            labelText: label,
            labelStyle: Theme.of(context).textTheme.bodyMedium,
            border: OutlineInputBorder(
              borderSide: BorderSide(
                color: Theme.of(context).primaryColorDark,
                width: 1,
              ),
            ),
            enabledBorder: OutlineInputBorder(
              borderSide: BorderSide(
                color: Theme.of(context).primaryColorDark,
                width: 1,
              ),
            ),
            focusedBorder: OutlineInputBorder(
              borderSide: BorderSide(
                color: Theme.of(context).colorScheme.secondary,
                width: 1,
              ),
            ),
            fillColor: Theme.of(context).primaryColorLight),
        style: Theme.of(context).textTheme.bodyMedium,
        items: options.map((String value) {
          print(value);
          return DropdownMenuItem<String>(
            value: value,
            child: Text(value),
          );
        }).toList(),
        onChanged: (String? value) {
          controller.text = value ?? '';
        },
      ),
    );
  }
}

2. 下拉框构建方法

Widget _countriesDropDown(BuildContext context, TextEditingController controller){
    return BlocProvider(
      create: (context) => _signupBloc,
      child: BlocBuilder<SignupBloc,SignupState>(
        buildWhen: ((previous, current) => previous is! Loaded && current is Loaded),
        builder: (context, state){
          if (state is Initial){
            _signupBloc.add(LoadEvent());
          }
          else if (state is Loaded){
              countries = state.countries;
              return SelectInputField("Country", countries, controller: controller);
          }
          return CircularProgressIndicator(color: Theme.of(context).colorScheme.secondary,);

        },
      )

    );
  }

3. Bloc实现类

class SignupBloc extends Bloc<SignupEvent, SignupState> {
  static const String countriesUrl = "https://api.first.org/data/v1/countries";

  SignupBloc() : super(Initial()) {

    on<LoadEvent>((event, emit) async{
      emit(Loading());

      List<String> countries = [];

      try{
        final response = await http.get(Uri.parse(countriesUrl));
        if(response.statusCode == 200){
          var responseJson = jsonDecode(response.body);
          responseJson['data'].values.forEach((element) {
           countries.add(element['country']);
          });
          emit(Loaded(countries));
        }
        else{
          emit(Failed("Error in getting data"));
        }
      }
      catch(e){
        print(e);
        emit(Failed("Failed to access internet. Try again later!"));
      }
    });
}

错误信息

======== Exception caught by rendering library =====================================================
The following assertion was thrown during paint():
Tried to paint a RenderObject reentrantly.

The following RenderObject was already being painted when it was painted again: RenderPointerListener#c100e relayoutBoundary=up20 NEEDS-PAINT
...  needs compositing
...  parentData: <none> (can use size)
...  constraints: BoxConstraints(0.0<=w<=351.4, 0.0<=h<=Infinity)
...  size: Size(351.4, 64.0)
...  behavior: opaque
...  listeners: down, panZoomStart
Since this typically indicates an infinite recursion, it is disallowed.
The relevant error-causing widget was: 
  DropdownButtonFormField<String> DropdownButtonFormField:file:///D:/Projects/sem5/iblock/lib/widgets/forms/select_input_field.dart:20:14
When the exception was thrown, this was the stack: 
#0      RenderObject._paintWithContext.<anonymous closure> (package:flutter/src/rendering/object.dart:2693:9)
#1      RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2706:6)
#2      PaintingContext.paintChild (package:flutter/src/rendering/object.dart:239:13)
#3      RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:144:15)
#4      RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2796:7)
#5      PaintingContext.paintChild (package:flutter/src/rendering/object.dart:239:13)
#6      RenderProxyBoxMixin.paint (package:flutter/src/rendering/proxy_box.dart:144:15)
#7      RenderObject._paintWithContext (package:flutter/src/rendering/object.dart:2796:7)

Flutter环境信息

[√] Flutter (Channel stable, 3.3.2, on Microsoft Windows [Version 10.0.22621.608], locale en-US)
    • Flutter version 3.3.2 on channel stable at C:\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision e3c29ec00c (3 weeks ago), 2022-09-14 08:46:55 -0500
    • Engine revision a4ff2c53d8
    • Dart version 2.18.1
    • DevTools version 2.15.0

[√] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at C:\Users\ISHAD\AppData\Local\Android\sdk
    • Platform android-32, build-tools 30.0.3
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • CHROME_EXECUTABLE = C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe

[√] Visual Studio - develop for Windows (Visual Studio Build Tools 2019 16.11.18)
    • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools
    • Visual Studio Build Tools 2019 version 16.11.32802.440
    • Windows 10 SDK version 10.0.19041.0

[√] Android Studio (version 2021.3)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[√] IntelliJ IDEA Ultimate Edition (version 2022.2)
    • IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA 2022.1.2
    • Flutter plugin version 70.2.5
    • Dart plugin version 222.4167.21

[√] VS Code (version 1.71.2)
    • VS Code at C:\Users\ISHAD\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.48.0

[√] Connected device (3 available)
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.22621.608]
    • Chrome (web)      • chrome  • web-javascript • unknown
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 105.0.1343.53
    ! Device DRGGAM5890805202 is not authorized.
      You might need to check your device for an authorization dialog.

[√] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

问题分析与修复方案

核心问题

  1. BlocProvider滥用导致状态循环:在_countriesDropDown方法中重复返回同一个_signupBloc实例,同时在BlocBuilder的builder回调中触发LoadEvent,结合buildWhen的过滤条件,引发组件反复重建和状态更新。
  2. builder中修改外部变量:countries = state.countries会触发组件重新build,加剧循环。
  3. 事件触发时机错误:在builder中触发事件,每次组件build到Initial状态都会重复发起请求,导致状态频繁变化。

修复步骤

1. 调整Bloc初始化与事件触发时机

在Bloc构造函数中自动触发加载事件,避免在builder中手动触发:

class SignupBloc extends Bloc<SignupEvent, SignupState> {
  static const String countriesUrl = "https://api.first.org/data/v1/countries";

  SignupBloc() : super(Initial()) {
    // 初始化时自动加载数据
    add(LoadEvent());

    on<LoadEvent>((event, emit) async{
      emit(Loading());

      List<String> countries = [];

      try{
        final response = await http.get(Uri.parse(countriesUrl));
        if(response.statusCode == 200){
          var responseJson = jsonDecode(response.body);
          responseJson['data'].values.forEach((element) {
           countries.add(element['country']);
          });
          emit(Loaded(countries));
        }
        else{
          emit(Failed("Error in getting data"));
        }
      }
      catch(e){
        print(e);
        emit(Failed("Failed to access internet. Try again later!"));
      }
    });
}

2. 重构下拉框构建方法

移除内部的BlocProvider(将BlocProvider提升到父组件),同时去掉外部变量修改和不必要的buildWhen:

Widget _countriesDropDown(BuildContext context, TextEditingController controller){
  return BlocBuilder<SignupBloc,SignupState>(
    builder: (context, state){
      if (state is Initial || state is Loading) {
        return CircularProgressIndicator(color: Theme.of(context).colorScheme.secondary);
      } else if (state is Loaded) {
        // 直接使用state中的数据,不修改外部变量
        return SelectInputField("Country", state.countries, controller: controller);
      } else if (state is Failed) {
        return Text((state as Failed).message);
      }
      return SizedBox.shrink();
    },
  );
}

3. 在父组件中提供Bloc实例

确保在页面层级提供Bloc,避免重复创建:

@override
Widget build(BuildContext context) {
  return BlocProvider(
    create: (context) => SignupBloc(),
    child: Scaffold(
      body: // 页面内容,调用_countriesDropDown方法
    ),
  );
}

4. 优化自定义下拉框组件

移除options.map中的print(value),避免大数据量下的性能问题。


内容的提问来源于stack exchange,提问作者Mohamed Ishad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 09:20:31