Flutter桌面CRM切换Tab时如何防止Widget重建避免数据丢失?
解决Fluent UI TabView切换标签时Widget重建导致数据丢失的问题
开发Flutter桌面端CRM时,采用Fluent UI库的TabView实现多标签功能,但切换至其他标签后返回原标签时,Flutter会重建整个Widget,导致“Appointments”标签内文本输入框的数据丢失。以下是具体的解决方案:
环境信息
flutter doctor 输出
[√] Flutter (Channel stable, 3.0.5, on Microsoft Windows [Version 10.0.19044.1889], locale en-US) [√] Android toolchain - develop for Android devices (Android SDK version 31.0.0) [√] Chrome - develop for the web [√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.2.6) [√] Android Studio (version 2020.3) [√] VS Code (version 1.70.2) [√] VS Code, 64-bit edition (version 1.20.1) [√] Connected device (3 available) [√] HTTP Host Availability
flutter --version 输出
Flutter 3.0.5 • channel stable • https://github.com/flutter/flutter.git Framework • revision f1875d570e (6 weeks ago) • 2022-07-13 11:24:16 -0700 Engine • revision e85ea0e79c Tools • Dart 2.17.6 • DevTools 2.12.2
问题代码片段
home.dart(TabView宿主)
class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); @override State<Home> createState() => _HomeState(); } class _HomeState extends State<Home>{ //TODO replace isLoaded with false bool isLoaded = true; bool serverProblem = false; int currentIndex = 0; final AppointmentGlobalKey = GlobalKey(); final DashboardGlobalKey = GlobalKey(); late Map<String, Map<String, dynamic>> mapScreenRoutes; @override void initState() { mapScreenRoutes = { App.appointmentsKey: { "title": "Appointments", "screen": AppointmentsScreen(), }, App.dashboardKey: { "title": "Dashboard", "screen": DashBoardScreen(), }, }; super.initState(); } @override Widget build(BuildContext context) { return isLoaded == false ? LoadingScreen(serverProblem: serverProblem) : Scaffold( body: Row( children: [ const SizedBox( width: 260, height: double.infinity, child: VerticalNavBar(), ), Expanded( child: Column( children: [ const HeaderSearchAccount(), Expanded( child: SizedBox( child: fui.TabView( closeButtonVisibility: fui.CloseButtonVisibilityMode.always, currentIndex: currentIndex, onChanged: (index) => setState(() { currentIndex = index; }), onNewPressed: null, tabs: [ fui.Tab( text: Text( mapScreenRoutes[App.appointmentsKey] ?['title']), closeIcon: fui.FluentIcons.chrome_close, ), fui.Tab( text: Text(mapScreenRoutes[App.dashboardKey] ?['title']), closeIcon: fui.FluentIcons.chrome_close, ) ], bodies: [ AppointmentsScreen( key: AppointmentGlobalKey, ), DashBoardScreen( key: DashboardGlobalKey, ), ]), ), ), ], ), ) ], ), ); } }
appointments_screen.dart
class AppointmentsScreen extends StatefulWidget { const AppointmentsScreen({Key? key}) : super(key: key); @override State<AppointmentsScreen> createState() => _AppointmentsScreenState(); } class _AppointmentsScreenState extends State<AppointmentsScreen> { bool isLoaded = false; bool serverProblem = false; @override void didChangeDependencies() { Api.get().fetchTextTest().then((value) { print(value); setState(() { isLoaded = true; }); }); super.didChangeDependencies(); } @override Widget build(BuildContext context) { final emailController = TextEditingController(); final formKey = GlobalKey<FormState>(); return isLoaded == false ? LoadingScreen(serverProblem: serverProblem) : Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Appointments()'), Center( child: Form( key: formKey, child: TextFormField( keyboardType: TextInputType.emailAddress, controller: emailController, autofocus: false, decoration: InputDecoration( hintText: 'Email', contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), border: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0)), ), ), ), ), ], ); } }
dashboard_screen.dart
class DashBoardScreen extends StatefulWidget { const DashBoardScreen({Key? key}) : super(key: key); @override State<DashBoardScreen> createState() => _DashBoardScreenState(); } class _DashBoardScreenState extends State<DashBoardScreen> with AutomaticKeepAliveClientMixin { //TODO replace isLoaded with false bool isLoaded = true; bool serverProblem = false; @override void initState() { super.initState(); } @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { return isLoaded == false ? LoadingScreen(serverProblem: serverProblem) : Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('DashBoardScreen()'), ], ); } }
解决方案
要实现切换标签时不重建Widget并保留数据,需要从以下几个方面修改:
1. 为AppointmentsScreen添加状态保活
你的DashBoardScreen已经使用了AutomaticKeepAliveClientMixin来保活状态,但AppointmentsScreen没有。给_AppointmentsScreenState混入这个mixin,并重写wantKeepAlive返回true,同时在build方法中调用super.build(context)(这是mixin要求的步骤)。
2. 修正控制器和全局键的声明位置
你在build方法内部声明了emailController和formKey,这会导致每次Widget重建时都会创建新的实例,输入数据自然会丢失。需要把这些变量移到State类的成员变量位置。
3. 优化API请求逻辑
didChangeDependencies会在多种情况下被调用,比如依赖变化时,这会导致重复请求API。添加判断逻辑,仅在未加载时发起请求。
修改后的AppointmentsScreen代码
class AppointmentsScreen extends StatefulWidget { const AppointmentsScreen({Key? key}) : super(key: key); @override State<AppointmentsScreen> createState() => _AppointmentsScreenState(); } class _AppointmentsScreenState extends State<AppointmentsScreen> with AutomaticKeepAliveClientMixin { // 添加状态保活mixin bool isLoaded = false; bool serverProblem = false; // 将控制器和全局键移到成员变量,避免每次build重建 final emailController = TextEditingController(); final formKey = GlobalKey<FormState>(); @override void didChangeDependencies() { // 仅在未加载时发起API请求,避免重复调用 if (!isLoaded) { Api.get().fetchTextTest().then((value) { print(value); setState(() { isLoaded = true; }); }).catchError((error) { setState(() { serverProblem = true; }); }); } super.didChangeDependencies(); } // 重写方法,标记需要保留状态 @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // 必须调用super.build,满足mixin要求 return isLoaded == false ? LoadingScreen(serverProblem: serverProblem) : Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Appointments()'), Center( child: Form( key: formKey, child: TextFormField( keyboardType: TextInputType.emailAddress, controller: emailController, autofocus: false, decoration: InputDecoration( hintText: 'Email', contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), border: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0)), ), ), ), ), ], ); } // 销毁控制器,避免内存泄漏 @override void dispose() { emailController.dispose(); super.dispose(); } }
额外说明
你在Home组件中为两个标签页设置了全局键,这有助于Flutter识别Widget实例,避免不必要的重建,当前写法正确,可以保留。
内容的提问来源于stack exchange,提问作者Christoforos Pantazis
相关产品推荐
相关产品推荐

