设备方向变化时更新Riverpod状态报错的解决求助
错误信息
StateNotifierListenerError(当StateNotifier实例'KeyBoardResize'尝试更新状态时,至少有一个监听器抛出异常。抛出的异常为:
尝试在Widget树构建期间修改Provider。
出现此错误的原因可能是你在Widget生命周期(包括但不限于build、initState、dispose、didUpdateWidget、didChangeDepedencies)中修改了Provider。不允许在这些生命周期内修改Provider,否则可能导致UI状态不一致,例如两个监听同一Provider的Widget可能接收到不同的状态。
问题代码
StateNotifier逻辑
class KeyBoardResize extends StateNotifier<bool> { KeyBoardResize() : super(false); changeBool(mode) { if (mode == Orientation.landscape) { return state = true; } else if (mode == Orientation.portrait) { return state = false; } } } final keyboardResizeNotifier = StateNotifierProvider<KeyBoardResize, bool>((ref) => KeyBoardResize());
Widget使用代码
class LoginScreen extends ConsumerWidget { const LoginScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final bool keyboardEnabled = WidgetsBinding.instance.window.viewInsets.bottom <= 0.0; final resize = ref.read(keyboardResizeNotifier); return Scaffold( resizeToAvoidBottomInset: resize, body: OrientationBuilder( builder: (context, orientation) { if (orientation == Orientation.portrait) { ref.read(keyboardResizeNotifier.notifier).changeBool(orientation); return portraitMode(context, keyboardEnabled); } else if (orientation == Orientation.landscape) { ref.read(keyboardResizeNotifier.notifier).changeBool(orientation); return landscapeMode(context, keyboardEnabled); } else { return Container(); } }, ), ); } }
解决方法
核心原因
错误本质是在Widget构建过程中(OrientationBuilder的builder方法属于build生命周期)直接修改了StateNotifier的状态,违反了Riverpod的规则——不允许在构建阶段更新Provider状态,否则会导致UI状态不一致。
方法1:延迟到构建完成后更新状态
使用WidgetsBinding.instance.addPostFrameCallback,将状态更新操作推迟到当前帧构建完成后执行:
修改OrientationBuilder的builder逻辑:
body: OrientationBuilder( builder: (context, orientation) { // 延迟到构建完成后更新状态 WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(keyboardResizeNotifier.notifier).changeBool(orientation); }); if (orientation == Orientation.portrait) { return portraitMode(context, keyboardEnabled); } else if (orientation == Orientation.landscape) { return landscapeMode(context, keyboardEnabled); } else { return Container(); } }, ),
方法2:让StateNotifier自动监听方向变化(推荐)
把方向监听逻辑封装到StateNotifier内部,无需在Widget中手动触发状态更新,更符合Riverpod的设计思想:
修改StateNotifier:
class KeyBoardResize extends StateNotifier<bool> { final Ref ref; late final VoidCallback _orientationListener; KeyBoardResize(this.ref) : super(false) { // 初始化时设置方向监听 _orientationListener = () { final orientation = MediaQuery.of(ref.context).orientation; // 直接根据方向更新状态 state = orientation == Orientation.landscape; }; WidgetsBinding.instance.addOrientationListener(_orientationListener); } // 销毁时移除监听,避免内存泄漏 @override void dispose() { WidgetsBinding.instance.removeOrientationListener(_orientationListener); super.dispose(); } }
更新Provider定义:
final keyboardResizeNotifier = StateNotifierProvider<KeyBoardResize, bool>((ref) => KeyBoardResize(ref));
修改Widget代码:
无需在OrientationBuilder中手动调用changeBool,直接监听状态即可:
class LoginScreen extends ConsumerWidget { const LoginScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final bool keyboardEnabled = WidgetsBinding.instance.window.viewInsets.bottom <= 0.0; // 监听StateNotifier的状态变化 final resize = ref.watch(keyboardResizeNotifier); return Scaffold( resizeToAvoidBottomInset: resize, body: OrientationBuilder( builder: (context, orientation) { if (orientation == Orientation.portrait) { return portraitMode(context, keyboardEnabled); } else if (orientation == Orientation.landscape) { return landscapeMode(context, keyboardEnabled); } else { return Container(); } }, ), ); } }
方法3:直接在Widget中计算值(最简方案)
如果逻辑只是根据方向设置resizeToAvoidBottomInset,完全可以不用StateNotifier,直接在build中计算:
class LoginScreen extends ConsumerWidget { const LoginScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final bool keyboardEnabled = WidgetsBinding.instance.window.viewInsets.bottom <= 0.0; final orientation = MediaQuery.of(context).orientation; return Scaffold( // 直接根据方向设置值 resizeToAvoidBottomInset: orientation == Orientation.portrait, body: OrientationBuilder( builder: (context, orientation) { if (orientation == Orientation.portrait) { return portraitMode(context, keyboardEnabled); } else if (orientation == Orientation.landscape) { return landscapeMode(context, keyboardEnabled); } else { return Container(); } }, ), ); } }
内容的提问来源于stack exchange,提问作者Rakul Agn

