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

Flutter拖拽对话框转全屏时出现UncontrolledProviderScope错误求助

问题:拖拽对话框转全屏时Riverpod状态更新报错

问题场景

实现了一个带动画的对话框,内部展示MyProfile页面。对话框中页面能正常向服务器发送请求并接收数据,但拖拽对话框至顶部切换为全屏显示时,抛出以下错误:

This UncontrolledProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.

问题推测

怀疑是页面在对话框和新屏幕实例中重复发送请求导致冲突,问题代码位于MyProfile类中:

ref.read(profileProvider.notifier).send(
        method: HTTP.POST,
        endPoint: Server.$userProfile,
        parameters: {
          'uuid': '123',
        },
      );

完整源码

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:my_app/core/service/server.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'core/service/repository/request_repository.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    ProviderScope(child: MyApp()),
  );
}

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

class MyHome extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          GestureDetector(
            onLongPress: () {
              showGeneralDialog(
                context: context,
                barrierDismissible: true,
                barrierLabel: "hi",
                pageBuilder: (context, pAnim, sAnim) {
                  return const SafeArea(
                      child: FloatingDialog(
                        previewType: PreviewDialogsType.profile,
                      ));
                },
                transitionBuilder: (context, pAnim, sAnim, child) {
                  if (pAnim.status == AnimationStatus.reverse) {
                    return FadeTransition(
                      opacity: Tween(begin: 0.0, end: 0.0).animate(pAnim),
                      child: child,
                    );
                  } else {
                    return FadeTransition(
                      opacity: pAnim,
                      child: child,
                    );
                  }
                },
              );
            },
            child: Container(
              width: double.infinity,
              height: 50.0,
              color: Colors.indigoAccent,
              child: const Text('LongPress please'),
            ),
          ),
        ],
      ),
    );
  }
}

enum PreviewDialogsType {
  profile,
}

class FloatingDialog extends StatefulWidget {
  final PreviewDialogsType previewType;

  const FloatingDialog({super.key, required this.previewType});

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

class _FloatingDialogState extends State<FloatingDialog> with TickerProviderStateMixin {
  late double _dragStartYPosition;
  late double _dialogYOffset;

  late AnimationController _returnBackController;
  late Animation<double> _dialogAnimation;

  @override
  void initState() {
    super.initState();
    _dialogYOffset = 0.0;
    _returnBackController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1300))
      ..addListener(() {
        setState(() {
          _dialogYOffset = _dialogAnimation.value;
        });
      });
  }

  @override
  void dispose() {
    _returnBackController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget myContents = PreviewDialog(
      type: widget.previewType,
    );

    return Padding(
      padding: const EdgeInsets.only(
        top: 100.0,
        bottom: 10.0,
        left: 16.0,
        right: 16.0,
      ),
      child: Transform.translate(
        offset: Offset(0.0, _dialogYOffset),
        child: Column(
          children: <Widget>[
            const Icon(
              Icons.keyboard_arrow_up,
              color: Colors.white,
              size: 30.0,
            ),
            Expanded(
              child: GestureDetector(
                onVerticalDragStart: (dragStartDetails) {
                  _dragStartYPosition = dragStartDetails.globalPosition.dy;
                },
                onVerticalDragUpdate: (dragUpdateDetails) {
                  setState(() {
                    _dialogYOffset = (dragUpdateDetails.globalPosition.dy) - _dragStartYPosition;
                  });
                  if (_dialogYOffset < -130) {
                    Navigator.of(context).pop();
                    Navigator.of(context).push(
                      PageRouteBuilder(
                          pageBuilder: (context, pAnim, sAnim) => myContents,
                          transitionDuration: const Duration(milliseconds: 500),
                          transitionsBuilder: (context, pAnim, sAnim, child) {
                            if (pAnim.status == AnimationStatus.forward) {
                              return ScaleTransition(
                                scale: Tween(begin: 0.8, end: 1.0)
                                    .animate(CurvedAnimation(parent: pAnim, curve: Curves.elasticOut)),
                                child: child,
                              );
                            } else {
                              return child;
                            }
                          }),
                    );
                  }
                },
                onVerticalDragEnd: (dragEndDetails) {
                  _dialogAnimation = Tween(begin: _dialogYOffset, end: 0.0)
                      .animate(CurvedAnimation(parent: _returnBackController, curve: Curves.elasticOut));
                  _returnBackController.forward(from: _dialogYOffset);

                  _returnBackController.forward(from: 0.0);
                },
                child: myContents,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PreviewDialog extends StatefulWidget {
  final PreviewDialogsType type;
  final String? uuid;

  const PreviewDialog({Key? key, required this.type, this.uuid}) : super(key: key);

  @override
  State<PreviewDialog> createState() => _PreviewDialogState();
}

class _PreviewDialogState extends State<PreviewDialog> {
  late ScrollController scrollController;

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.rtl,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(10.0),
        child: Scaffold(
          body: MyProfile(),
        ),
      ),
    );
  }
}

class MyProfile extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    useEffect(() {
      ref.read(profileProvider.notifier).send(
        method: HTTP.POST,
        endPoint: Server.$userProfile,
        parameters: {
          'uuid': '123',
        },
      );
      return () {};
    }, []);

    return SafeArea(
      child: Scaffold(
        body: Container(
          width: double.infinity,
          height:double.infinity,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              ref.watch(profileProvider).when(
                idle: () => const SizedBox(),
                loading: () => const CircularProgressIndicator(),
                success: (success) {
                  return const Text('successful');
                },
                error: (error, stackTrace) {
                  return Text('error');
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

解决方案

1. 避免重复创建Widget实例

FloatingDialog的build方法每次执行都会重新创建myContents实例,拖拽跳转时该实例已处于对话框构建流程中,引发状态更新冲突。将实例创建移至initState,确保只初始化一次:

class _FloatingDialogState extends State<FloatingDialog> with TickerProviderStateMixin {
  late double _dragStartYPosition;
  late double _dialogYOffset;

  late AnimationController _returnBackController;
  late Animation<double> _dialogAnimation;
  late Widget myContents;

  @override
  void initState() {
    super.initState();
    _dialogYOffset = 0.0;
    // 仅初始化一次页面实例
    myContents = PreviewDialog(
      type: widget.previewType,
    );
    _returnBackController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1300))
      ..addListener(() {
        setState(() {
          _dialogYOffset = _dialogAnimation.value;
        });
      });
  }

  // 其余代码保持不变
}

2. 控制请求发送时机,避免重复请求

当前MyProfile的useEffect会在Widget初始化时触发请求,对话框和新页面实例都会执行,导致重复请求。可通过两种方式处理:

方式一:在Provider内部控制请求逻辑

让profileProvider的Notifier自动判断请求状态,仅在需要时发送:

final profileProvider = StateNotifierProvider<ProfileNotifier, AsyncValue<ProfileData>>((ref) {
  return ProfileNotifier();
});

class ProfileNotifier extends StateNotifier<AsyncValue<ProfileData>> {
  ProfileNotifier() : super(const AsyncValue.idle());

  Future<void> send({required HTTP method, required String endPoint, required Map<String, dynamic> parameters}) async {
    // 已加载或正在加载时,不重复请求
    if (state.isLoading || state.hasValue) return;
    
    state = const AsyncValue.loading();
    try {
      final data = await RequestRepository().request(method, endPoint, parameters);
      state = AsyncValue.data(data);
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
}

方式二:在MyProfile中判断状态后发送

class MyProfile extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final profileState = ref.watch(profileProvider);
    useEffect(() {
      // 仅当状态为idle时发送请求
      if (profileState is AsyncIdle) {
        ref.read(profileProvider.notifier).send(
          method: HTTP.POST,
          endPoint: Server.$userProfile,
          parameters: {
            'uuid': '123',
          },
        );
      }
      return () {};
    }, []);

    // 其余代码保持不变
  }
}

3. 避免在构建流程中触发页面跳转

拖拽更新时的setState会触发FloatingDialog的build,此时执行页面跳转可能引发状态更新冲突。将跳转逻辑移至onVerticalDragEnd,确保在构建流程结束后执行:

onVerticalDragUpdate: (dragUpdateDetails) {
  setState(() {
    _dialogYOffset = (dragUpdateDetails.globalPosition.dy) - _dragStartYPosition;
  });
},
onVerticalDragEnd: (dragEndDetails) {
  if (_dialogYOffset < -130) {
    Navigator.of(context).pop();
    Navigator.of(context).push(
      PageRouteBuilder(
          pageBuilder: (context, pAnim, sAnim) => myContents,
          transitionDuration: const Duration(milliseconds: 500),
          transitionsBuilder: (context, pAnim, sAnim, child) {
            if (pAnim.status == AnimationStatus.forward) {
              return ScaleTransition(
                scale: Tween(begin: 0.8, end: 1.0)
                    .animate(CurvedAnimation(parent: pAnim, curve: Curves.elasticOut)),
                child: child,
              );
            } else {
              return child;
            }
          }),
    );
  } else {
    _dialogAnimation = Tween(begin: _dialogYOffset, end: 0.0)
        .animate(CurvedAnimation(parent: _returnBackController, curve: Curves.elasticOut));
    _returnBackController.forward(from: 0.0);
  }
},

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 16:01:15