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做初始化状态判断,反而会增加重建触发概率
修复方案
移除重复的Firebase初始化代码
直接删掉App组件State中final Future<FirebaseApp> _initialization = Firebase.initializeApp();这行重复定义,不需要在组件内二次调用Firebase初始化。替换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(); }复用提前定义的pages列表
PageView的children属性直接使用State中提前定义的pages列表,不要每次build都重新创建页面实例:PageView( physics: AlwaysScrollableScrollPhysics(), onPageChanged: (int index) { setState(() { _selectedIndex = index; }); }, controller: _pageController, pageSnapping: true, children: pages, // 直接复用提前创建的pages列表 )给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
相关产品推荐
相关产品推荐

