Flutter异步重建Widget报错:build期间调用setState/markNeedsBuild
解决方案
问题根源
你在build方法中直接调用playScale,而playScale通过同步while循环调用playNoteFromIndex,后者会立即触发notifyListeners()——此时Widget正处于构建阶段,触发状态更新就会抛出setState() or markNeedsBuild called during build异常。
方案1:推迟playScale执行到构建完成后
使用WidgetsBinding.instance.addPostFrameCallback,确保UI构建完成后再执行音阶播放逻辑,避免在build期间触发状态更新:
@override Widget build(BuildContext context) { // 监听音阶变化 final scale = context.watch<ScaleProvider>().currentScale; // 推迟到build完成后执行播放 WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { playScale(context, scale); } }); return Consumer<PianoKeyProvider>( builder: (context, pianoKeyProvider, child) { // 构建钢琴按键UI return ...; }, ); }
方案2:将playScale改为异步函数,避免同步阻塞
原来的while循环是同步执行的,会连续触发多次notifyListeners(),导致UI来不及处理。改成异步循环,每次播放后等待足够时间,让UI完成更新:
Future<void> playScale(BuildContext context, List<int> scaleNotes) async { final pianoKeyProvider = context.read<PianoKeyProvider>(); int index = 0; while (index < scaleNotes.length && context.mounted) { final midiNote = scaleNotes[index]; // 播放当前音符 await pianoKeyProvider.playNoteFromIndex(index, midiNote); // 等待音符播放完成(和你设置的500ms恢复颜色对应) await Future.delayed(const Duration(milliseconds: 600)); index++; } }
同时修改PianoKeyProvider的playNoteFromIndex为异步方法,确保延迟恢复颜色的逻辑正确:
class PianoKeyProvider extends ChangeNotifier { List<Color> _colorList = ...; Future<void> playNoteFromIndex(int noteIndex, int midiNote) async { // 修改颜色并通知更新 _colorList[noteIndex] = Colors.red; notifyListeners(); // 播放MIDI音符 playMidiNote(midiNote); // 延迟恢复颜色,确保UI已完成更新 await Future.delayed(const Duration(milliseconds: 500)); if (_colorList[noteIndex] == Colors.red) { _colorList[noteIndex] = Colors.white; notifyListeners(); } } }
方案3:避免在build中直接触发业务逻辑(推荐)
更好的实践是将音阶变化的监听移到initState或使用addListener,而不是在build中每次都触发播放:
class PianoScreen extends StatefulWidget { @override State<PianoScreen> createState() => _PianoScreenState(); } class _PianoScreenState extends State<PianoScreen> { late ScaleProvider _scaleProvider; @override void initState() { super.initState(); _scaleProvider = context.read<ScaleProvider>(); // 监听音阶变化,仅当音阶改变时触发播放 _scaleProvider.addListener(_onScaleChanged); } @override void dispose() { _scaleProvider.removeListener(_onScaleChanged); super.dispose(); } void _onScaleChanged() async { if (!mounted) return; final scale = _scaleProvider.currentScale; await playScale(context, scale); } @override Widget build(BuildContext context) { return Consumer<PianoKeyProvider>( builder: (context, pianoKeyProvider, child) { return ...; // 构建UI }, ); } }
这样可以避免build方法每次执行都触发播放,同时确保播放逻辑在UI稳定后执行。
内容的提问来源于stack exchange,提问作者AudioFrank
相关产品推荐
相关产品推荐

