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

Flutter仿TikTok竖屏滑动页 离开页面视频后台播放问题修复

问题根源
  • PageView默认会缓存当前页前后各1个子组件,滑动切页时,离开页面的VideoPlayerContent不会立即触发dispose,原有自动播放逻辑在initState执行后没有后续状态控制,视频会持续在后台播放。
  • 原有代码没有监听PageView的页面切换事件,无法在页面滑出可视区域时主动触发暂停、滑入时触发播放。
  • 原有代码没有监听应用生命周期,当Reels页面被压入后台(跳转其他页面、按Home键返回桌面)时,视频不会自动暂停。
  • 附带代码bug:showDialogueBox方法调用时第一个参数传入了BuildContext类型而非上下文实例,会触发语法报错。
修复步骤

1. 改造Reels主页面

把PageController从build方法提升到State层级,新增当前激活页索引变量,给控制器添加页面滑动监听,切页时更新索引并触发组件刷新。
修改后的_ReelsState代码如下:

class _ReelsState extends State<Reels> {
  // 提升PageController到State层级,避免build时重复创建
  late final PageController _pageController;
  // 记录当前激活的视频页索引
  int _currentActivePage = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(viewportFraction: 1, initialPage: 0);
    // 监听页面滑动事件
    _pageController.addListener(() {
      final page = _pageController.page?.round() ?? 0;
      if (page != _currentActivePage) {
        setState(() {
          _currentActivePage = page;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        automaticallyImplyLeading: false,
        elevation: 0,
        flexibleSpace: SizedBox(
          height: 200,
          child: Padding(
            padding: const EdgeInsets.all(15.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                IconButton(onPressed: () => showDialogueBox(context), // 修复原代码传BuildContext类型的bug
                    icon: const Icon(Icons.add),
                  iconSize: 35,
                  color: Colors.teal[300],
                )
              ],
            ),
          ),
        ),
        backgroundColor: Colors.transparent,

      ),
      body: StreamBuilder(
        stream: fireStore.collection("videoReels").snapshots(),
        builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
          return PageView.builder(
            itemCount: snapshot.data!.docs.length,
            scrollDirection: Axis.vertical,
            controller: _pageController, // 使用State中定义的控制器
            itemBuilder: (context, index){
              DocumentSnapshot dataSnapshot = snapshot.data!.docs[index];
              return Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  // 传入当前页是否激活的标记
                  VideoPlayerContent(
                    videoUrl: dataSnapshot["videoUrl"],
                    isActive: index == _currentActivePage,
                  ),
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Expanded(
                        flex: 3,
                        child: Container(
                          height: MediaQuery.of(context).size.height/6,
                          padding: const EdgeInsets.all(10.0),
                          child: Column(
                            children: [
                              Row(
                                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                                crossAxisAlignment: CrossAxisAlignment.center,
                                children: [
                                  Row(
                                    children: [
                                      InkWell(
                                        onTap: () => {},
                                        child: const Icon(Icons.thumb_up, color: Colors.blueAccent,),
                                      ),
                                      const SizedBox(width: 30,),
                                      InkWell(
                                        onTap: () => {},
                                        child: const Icon(Icons.favorite, color: Colors.red,),
                                      )
                                    ],
                                  ),
                                  const SizedBox(width: 20,),
                                  InkWell(
                                    onTap: () => {},
                                    child: Text(" Add Comment here... ", style: TextStyle( color: Colors.grey[500] ),),
                                  )
                                ],
                              ),
                              const SizedBox(height: 30,),
                              Padding(
                                padding: const EdgeInsets.fromLTRB(30.0, 4.0,30.0,4.0),
                                child: Row(
                                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                  crossAxisAlignment: CrossAxisAlignment.center,
                                  children: [
                                    InkWell(
                                      onTap: () => { } ,
                                      child:ClipRRect(
                                        borderRadius: BorderRadius.circular(50.0),
                                        child: CircleAvatar(backgroundImage: NetworkImage(dataSnapshot['avatar']), radius: 25,),
                                      ),
                                    ),
                                    Text((dataSnapshot["videoUrl"])),
                                    ElevatedButton(onPressed: ()=> {}, child: const Text("Subscribe"))
                                  ],
                                ),
                              )
                            ],
                          ),
                        ),
                      ),
                    ],
                  )
                ],
              );
            },
          );
        }
      ),
    );
  }
}

2. 改造VideoPlayerContent组件

新增isActive入参,在组件状态更新时判断当前是否为激活页,自动控制播放/暂停;同时添加应用生命周期监听,App退到后台时自动暂停视频。
修改后的代码如下:

import 'package:app/packageManager/package.dart';

class VideoPlayerContent extends StatefulWidget {
  final String videoUrl;
  // 新增是否为当前激活页的参数
  final bool isActive;
  const VideoPlayerContent({Key? key, required this.videoUrl, required this.isActive}) : super(key: key);
  @override
  State<VideoPlayerContent> createState() => _VideoPlayerContentState();
}

// 添加WidgetsBindingObserver监听应用生命周期
class _VideoPlayerContentState extends State<VideoPlayerContent> with WidgetsBindingObserver {
  late VideoPlayerController _videoController;
  late Future _initializeVideoPlayer;

  @override
  void initState(){
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _videoController = VideoPlayerController.network(widget.videoUrl);
    _initializeVideoPlayer =  _videoController.initialize().then((_) {
      // 初始化完成后如果是当前激活页才播放
      if (widget.isActive) {
        _videoController.play();
      }
      _videoController.setVolume(1);
      _videoController.setLooping(true);
      setState(() {});
    });
  }

  // 监听组件配置更新,切页时触发
  @override
  void didUpdateWidget(covariant VideoPlayerContent oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 页面激活状态变化时控制播放暂停
    if (oldWidget.isActive != widget.isActive) {
      if (widget.isActive) {
        _videoController.play();
      } else {
        _videoController.pause();
      }
    }
  }

  // 监听应用生命周期变化
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    // 应用退到后台、不可见时暂停视频
    if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) {
      _videoController.pause();
    }
    // 应用回到前台且当前是激活页时恢复播放
    if (state == AppLifecycleState.resumed && widget.isActive) {
      _videoController.play();
    }
  }

  @override
  void dispose (){
    WidgetsBinding.instance.removeObserver(this);
    _videoController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: MediaQuery.of(context).size.height,
      width: MediaQuery.of(context).size.width,
      child: FutureBuilder(
        future: _initializeVideoPlayer,
        builder: (context, snapshot){
          if (snapshot.connectionState == ConnectionState.done){
            return VideoPlayer(_videoController);
          }else{
            return Container(
              color: Colors.black,
              child: const Center(
                child:  CircularProgressIndicator(
                  value: 0.8,
                  valueColor: AlwaysStoppedAnimation<Color>(Colors.purpleAccent),
                ),
              )
            );
          }
        },
      ),
    );
  }
}
额外优化建议
  • 如果需要减少内存占用,可以给PageView.builder添加cacheExtent: 0参数,禁用前后页缓存,滑走的页面会立即dispose释放播放器资源,注意这个配置会让滑到下一个视频时需要重新初始化加载,可根据需求取舍。
  • 可以给视频添加点击暂停/播放的交互,只需要在VideoPlayer外层套GestureDetector,点击时判断_controller.value.isPlaying执行pause/play即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 18:36:51