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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 23:33:25