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

Flutter BottomSheet内Slider.adaptive不更新,_position数据无法传递

问题描述

我在BottomSheet中使用Slider.adaptive组件,即便已经使用了StatefulBuilder,视图仍无法更新。简化后的代码如下:

Duration _duration = const Duration(milliseconds: 0);
Duration _position = const Duration(milliseconds: 0);

@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,
    onPopInvoked: (bool didPop) async {
      if (isPlaying == true) {
        setState(() async {
          await audioPlayer.stop();
        });
      }
    },
    child: Scaffold(
      body: SafeArea(
        child: Center(
          child: Text('Testing Screen'),
        )
      ),
      bottomNavigationBar: BottomAppBar(
        padding: EdgeInsets.zero,
        color: Colors.black,
        height: 130,
        child: Column(
          children: [
            IconButton(
              icon: const FaIcon(
                FontAwesomeIcons.headphones,
                color: Colors.white70,
              ),
              onPressed: () async {
                _openAudioBottomSheet();
              },
            ),
          ]
        ),
      ),
    ),
  );
}

void  _openAudioBottomSheet() {
  showModalBottomSheet(
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
      ),
      context: context,
      showDragHandle: true,
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Slider.adaptive(
                      min: 0,
                      max: _duration.inSeconds.toDouble(),
                      value: _position.inSeconds.toDouble(),
                      onChanged: (value) async {
                        debugPrint('value is:: $value');
                        setState(() {
                          _position = Duration(seconds: value.toInt());
                        });
                      },
                    ),
                  ],
                ),
              ),
            );
          }
        );
      }
  );
}

请问如何解决视图更新的问题?

更新

我刚刚发现问题大概率并非onChanged方法的问题,而是_position数据无法传递到showModalBottomSheet中!看起来_position的更新时长无法同步到底部弹窗,导致滑块无法前进。以下是我的音频播放器初始化代码:

@override
  void initState() {
    super.initState();
    initAudioPlayer();
  }

  initAudioPlayer() async {
    audioPlayer.setAudioContext(
        const AudioContext(
          android: AudioContextAndroid(),
          iOS: AudioContextIOS(),
        )
    );

    audioPlayer.onPlayerStateChanged.listen((PlayerState event) {
      setState(() {
        isPlaying = event == PlayerState.playing;
        audioPlayer.setVolume(_volume);

        if (PlayerState == PlayerState.completed || PlayerState == PlayerState.stopped || PlayerState == PlayerState.paused) {
          setState(() {
            isLoading = false; // Loading is complete
          });
        } else if (PlayerState == PlayerState.playing) {
          setState(() {
            isLoading = true; // Audio loading is in progress
          });
        }
      });
    });

    audioPlayer.onDurationChanged.listen((Duration event) {
      setState(() {
        _duration = event;
      });
    });

    audioPlayer.onPositionChanged.listen((Duration event) {
      // this does get updated, but wont pass to bottomsheet!
      debugPrint('value is: $event');
      setState(() {
        _position = event;
      });
    });
  }

解决方法

问题核心是:showModalBottomSheet创建的弹窗是独立于原页面的Widget树分支,原页面的setState只能更新原页面UI,无法触发弹窗内StatefulBuilder的刷新;同时弹窗初始化时捕获的_position是静态快照,后续原页面的_position更新无法自动同步到弹窗。

方法一:用ValueNotifier实现跨Widget状态共享

这是最简洁的方案,通过ValueNotifier实现状态的全局监听:

  1. 将原页面的_position和_duration替换为ValueNotifier:
final ValueNotifier<Duration> _positionNotifier = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> _durationNotifier = ValueNotifier(Duration.zero);
  1. 修改音频监听逻辑,直接更新ValueNotifier:
audioPlayer.onPositionChanged.listen((Duration event) {
  debugPrint('value is: $event');
  _positionNotifier.value = event;
});

audioPlayer.onDurationChanged.listen((Duration event) {
  _durationNotifier.value = event;
});
  1. 弹窗中使用ValueListenableBuilder监听状态变化:
void _openAudioBottomSheet() {
  showModalBottomSheet(
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
    ),
    context: context,
    showDragHandle: true,
    builder: (BuildContext context) {
      return SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ValueListenableBuilder<Duration>(
                valueListenable: _durationNotifier,
                builder: (context, duration, child) {
                  return ValueListenableBuilder<Duration>(
                    valueListenable: _positionNotifier,
                    builder: (context, position, child) {
                      return Slider.adaptive(
                        min: 0,
                        max: duration.inSeconds.toDouble(),
                        value: position.inSeconds.toDouble(),
                        onChanged: (value) {
                          final newPosition = Duration(seconds: value.toInt());
                          _positionNotifier.value = newPosition;
                          // 同步到音频播放器
                          audioPlayer.seek(newPosition);
                        },
                      );
                    },
                  );
                },
              ),
            ],
          ),
        ),
      );
    }
  );
}

方法二:用GlobalKey控制弹窗内的状态刷新

如果不想用ValueNotifier,可以通过GlobalKey直接调用弹窗内的更新方法:

  1. 定义弹窗的独立State类和GlobalKey:
final GlobalKey<_BottomSheetSliderState> _sliderKey = GlobalKey();

class BottomSheetSlider extends StatefulWidget {
  final Duration initialPosition;
  final Duration initialDuration;

  const BottomSheetSlider({
    super.key,
    required this.initialPosition,
    required this.initialDuration,
  });

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

class _BottomSheetSliderState extends State<BottomSheetSlider> {
  late StateSetter _setState;
  late Duration _position;
  late Duration _duration;

  @override
  void initState() {
    super.initState();
    _position = widget.initialPosition;
    _duration = widget.initialDuration;
  }

  @override
  Widget build(BuildContext context) {
    return StatefulBuilder(
      builder: (context, setState) {
        _setState = setState;
        return Slider.adaptive(
          min: 0,
          max: _duration.inSeconds.toDouble(),
          value: _position.inSeconds.toDouble(),
          onChanged: (value) {
            _position = Duration(seconds: value.toInt());
            setState(() {});
            // 同步到原页面和音频播放器
            (context.findAncestorStateOfType<_YourPageState>() as _YourPageState)._position = _position;
            audioPlayer.seek(_position);
          },
        );
      },
    );
  }

  // 供外部调用的更新方法
  void updatePosition(Duration newPosition) {
    _setState(() {
      _position = newPosition;
    });
  }

  void updateDuration(Duration newDuration) {
    _setState(() {
      _duration = newDuration;
    });
  }
}
  1. 原页面音频监听中调用弹窗的更新方法:
audioPlayer.onPositionChanged.listen((Duration event) {
  debugPrint('value is: $event');
  setState(() {
    _position = event;
  });
  // 弹窗已打开时更新滑块
  if (_sliderKey.currentState != null) {
    _sliderKey.currentState!.updatePosition(event);
  }
});

audioPlayer.onDurationChanged.listen((Duration event) {
  setState(() {
    _duration = event;
  });
  if (_sliderKey.currentState != null) {
    _sliderKey.currentState!.updateDuration(event);
  }
});
  1. 打开弹窗时传入初始值和Key:
void _openAudioBottomSheet() {
  showModalBottomSheet(
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
    ),
    context: context,
    showDragHandle: true,
    builder: (BuildContext context) {
      return SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              BottomSheetSlider(
                key: _sliderKey,
                initialPosition: _position,
                initialDuration: _duration,
              ),
            ],
          ),
        ),
      );
    }
  );
}

额外修复点

  • 原代码onPopInvoked中setState包裹异步函数是错误的,应改为:
onPopInvoked: (bool didPop) async {
  if (isPlaying == true) {
    await audioPlayer.stop();
    setState(() {
      isPlaying = false;
    });
  }
},
  • onPlayerStateChanged中错误使用了PlayerState类型而非event变量,应改为:
if (event == PlayerState.completed || event == PlayerState.stopped || event == PlayerState.paused) {
  setState(() {
    isLoading = false;
  });
} else if (event == PlayerState.playing) {
  setState(() {
    isLoading = true;
  });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 05:44:53