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

Flutter PageView切页时FutureBuilder重复调用致卡顿如何解决

问题描述
  • Flutter开发过程中切换PageView页面时,FutureBuilder重复执行,导致页面导航性能严重下降,出现明显卡顿
  • 已尝试接入provider做状态管理、将FutureBuilder初始化逻辑移入initState保证仅调用一次,均未解决问题
  • 在FutureBuilder中firebase初始化完成位置添加日志后,观察到每次切换页面都会重复输出该日志,确认初始化逻辑被重复触发
  • 预期效果:FutureBuilder关联的初始化逻辑仅在initState阶段执行一次,消除重复调用带来的性能损耗
问题代码(main.dart)
NotificationService notificationService = NotificationService();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  notificationService.init();
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  runApp(AppWrapper());
}

class AppWrapper extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AppWrapperState();
  }
}

class _AppWrapperState extends State<AppWrapper> {
  @override
  Widget build(BuildContext context) {
    return OverlaySupport.global(
      child: MaterialApp(
          theme: ThemeData(
              textTheme: GoogleFonts.openSansTextTheme(
            Theme.of(context).textTheme,
          )),
          debugShowCheckedModeBanner: false,
          home: App()),
    );
  }
}

/// We are using a StatefulWidget such that we only create the [Future] once,
/// no matter how many times our widget rebuild.
/// If we used a StatelessWidget, in the event where [App] is rebuilt, that
/// would re-initialize FlutterFire and make our application re-enter loading state,
/// which is undesired.
class App extends StatefulWidget {
  // Create the initialization Future outside of `build`:
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  /// The future is part of the state of our widget. We should not call `initializeApp`
  /// directly inside [build].
  bool? _isLoggedIn;
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  void initState() {
    // initIsLoggedIn();
    super.initState();
    _asyncMethod();
  }

  void _asyncMethod() async {
    try {
      // Get values from storage
      final storage = new FlutterSecureStorage();
      String isLoggedIn = await storage.read(key: "isLoggedIn") ?? "";
      if (isLoggedIn == "true") {
        //If the user seems logged in, we check it by trying to authenticate
        String mail = await storage.read(key: "mail") ?? "";
        String password = await storage.read(key: "password") ?? "";
        UserResponse userResponse = await APIUser().login(mail, password);
        User? user = userResponse.user;
        if (user != null) {
          setState(() {
            _isLoggedIn = true;
          });
        } else {
          //If we cannot connect the user with the stored mail and password, then we redirect the user to the login page
          setState(() {
            _isLoggedIn = false;
          });
          print("returning to login page");
          Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (context) => LoginPage()),
          );
        }
      } else {
        //If we cannot connect the user with the stored mail and password, then we redirect the user to the login page
        setState(() {
          _isLoggedIn = false;
        });
        print("returning to login page");
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (context) => LoginPage()),
        );
      }
    } catch (e) {
      //print the error and redirect the user to the login page
      print(e);
      Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (context) => LoginPage()));
    }
  }

  //The following variables are used to handle the navigation between the 3 main pages
  List<Widget> pages = [
    SettingsPage(key: PageStorageKey('settings')),
    HomePage(key: PageStorageKey('home')),
    AccountPage(
      key: PageStorageKey('account'),
    ),
  ];
  int _selectedIndex = 1;

  // void _changePage(int index) {
  //   setState(() {
  //     _selectedIndex = index;
  //   });
  // }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          return somethingWentWrongWidget();
        }

        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done &&
            _isLoggedIn != null) {
          print("Initialize FireBase done");
          return appContentWidget();
        }

        // Otherwise, show something whilst waiting for initialization to complete
        print("loadingpage showing to let initialization load");
        return LoadingPage();
      },
    );
  }

  Widget somethingWentWrongWidget() {
    return Center(
      child: Text('Someting went wrong', textDirection: TextDirection.ltr),
    );
  }

  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
  PageController _pageController = PageController(
    initialPage: 1,
  );
  final _bottomNavigationItems = [
    BottomNavigationBarItem(
        activeIcon: ImageIcon(
          AssetImage("assets/settingsActive.png"),
          size: 32,
        ),
        icon: ImageIcon(
          AssetImage("assets/settings.png"),
          size: 32,
          color: Colors.black,
        ),
        label: ''),
    BottomNavigationBarItem(
        activeIcon: ImageIcon(
          AssetImage("assets/dashboardActive.png"),
          size: 32,
        ),
        icon: ImageIcon(
          AssetImage("assets/dashboard.png"),
          size: 32,
          color: Colors.black,
        ),
        label: ''),
    BottomNavigationBarItem(
        activeIcon: ImageIcon(
          AssetImage("assets/userActive.png"),
          size: 32,
          color: Colors.black,
        ),
        icon: ImageIcon(
          AssetImage("assets/user.png"),
          size: 32,
          color: Colors.black,
        ),
        label: ''),
  ];
  Widget appContentWidget() {
    double bottomNavbarHeight = 70;
    if (Platform.isIOS) {
      bottomNavbarHeight = 90;
    }
    // check if the user is logged in, if not go to login page
    bool loggedIn = _isLoggedIn ?? false;
    if (!loggedIn) {
      return LoginPage();
    } else {
      return WillPopScope(
        onWillPop: () {
          return Future.value(false);
        },
        child: Scaffold(
          key: _scaffoldKey,
          body: PageView(
            physics: AlwaysScrollableScrollPhysics(),
            onPageChanged: (int index) {
              setState(() {
                _selectedIndex = index;
              });
            },
            controller: _pageController,
            pageSnapping: true,
            children: [
              SettingsPage(key: PageStorageKey('settings')),
              HomePage(key: PageStorageKey('home')),
              AccountPage(
                key: PageStorageKey('account'),
              ),
            ],
          ),
          bottomNavigationBar: Container(
            height: bottomNavbarHeight,
            decoration: BoxDecoration(
                boxShadow: [BoxShadow(color: Colors.grey.shade200)]),
            child: Theme(
              data: ThemeData(
                brightness: Brightness.light,
                splashColor: Colors.transparent,
                highlightColor: Colors.transparent,
              ),
              child: BottomNavigationBar(
                type: BottomNavigationBarType.fixed,
                elevation: 20,
                backgroundColor: Colors.white,
                selectedFontSize: 0,
                //As the label is obligatory, we give it a size of zero
                unselectedFontSize: 0,
                //As the label is obligatory, we give it a size of zero
                items: _bottomNavigationItems,
                selectedItemColor: Colors.black,
                currentIndex: _selectedIndex,
                onTap: (int index) {
                  _pageController.animateToPage(index,
                      duration: Duration(milliseconds: 500),
                      curve: Curves.easeInCubic);
                },
              ),
            },
          ),
        ),
      );
    }
  }
}
问题根因
  • 存在重复Firebase初始化逻辑:main()函数中已经执行过await Firebase.initializeApp()完成全局初始化,App组件State中又定义了_initialization = Firebase.initializeApp()重复触发初始化流程
  • PageView子页面重复创建:虽然提前定义了带PageStorageKey的pages列表,但build方法中写PageView的children时又重新实例化了三个页面,没有复用提前创建的页面实例,切页时会触发子页面反复重建
  • PageView默认销毁离屏页面:默认配置下PageView会销毁滑出视口的页面,切页时子页面销毁重建会连带触发外层组件重建,打在FutureBuilder builder函数里的日志自然会重复打印
  • 不必要的FutureBuilder使用:Firebase已经在main函数中完成初始化,登录校验逻辑也在initState中执行,不需要额外嵌套FutureBuilder做初始化状态判断,反而会增加重建触发概率
修复方案
  1. 移除重复的Firebase初始化代码
    直接删掉App组件State中final Future<FirebaseApp> _initialization = Firebase.initializeApp();这行重复定义,不需要在组件内二次调用Firebase初始化。

  2. 替换FutureBuilder为普通状态标记位
    新增初始化完成标记位,在initState的异步登录校验逻辑执行完成后修改标记位,build方法直接根据标记位返回加载页/错误页/主页面,避免FutureBuilder builder反复执行:

    // 替换原有的_initialization变量
    bool _initComplete = false;
    bool _initError = false;
    
    void _asyncMethod() async {
      try {
        final storage = FlutterSecureStorage();
        String isLoggedIn = await storage.read(key: "isLoggedIn") ?? "";
        // 保留原有登录校验逻辑不变
        // ...
        // 所有逻辑执行完成后标记初始化完成
        if (mounted) {
          setState(() {
            _initComplete = true;
          });
        }
      } catch (e) {
        print(e);
        if (mounted) {
          setState(() {
            _initError = true;
          });
        }
      }
    }
    
    @override
    Widget build(BuildContext context) {
      if (_initError) {
        return somethingWentWrongWidget();
      }
      if (!_initComplete || _isLoggedIn == null) {
        return LoadingPage();
      }
      return appContentWidget();
    }
    
  3. 复用提前定义的pages列表
    PageView的children属性直接使用State中提前定义的pages列表,不要每次build都重新创建页面实例:

    PageView(
      physics: AlwaysScrollableScrollPhysics(),
      onPageChanged: (int index) {
        setState(() {
          _selectedIndex = index;
        });
      },
      controller: _pageController,
      pageSnapping: true,
      children: pages, // 直接复用提前创建的pages列表
    )
    
  4. 给PageView子页面添加保活逻辑
    给SettingsPage、HomePage、AccountPage三个页面的State类混入AutomaticKeepAliveClientMixin,避免离屏页面被销毁:

    // 以HomePage为例,其余两个页面做相同修改
    class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {
      @override
      bool get wantKeepAlive => true;
    
      @override
      Widget build(BuildContext context) {
        super.build(context); // 必须调用,否则保活不生效
        // 保留原有页面build逻辑
        return Scaffold(/* 原有页面内容 */);
      }
    }
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 17:33:40