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

点击TextFormField时键盘反复开合,Widget多次重建问题

问题分析与解决方案

核心问题

点击TextFormField时键盘弹出后立刻关闭,根源在于LoginScreen被频繁重建,导致输入框控制器实例被替换,焦点丢失。具体诱因如下:

  1. MyApp重建时会创建全新的LoginScreen实例
    MyApp中使用context.watch<ThemeCubit>()监听主题变化,每次主题切换都会触发MyApp重建;同时Android平台的FutureBuilder完成、AuthenticationBloc状态变更时,BlocBuilder也会重建。每次重建都会返回LoginScreen(),生成全新的LoginScreen对象,原有的TextEditingController被销毁,输入框失去焦点。

  2. LoginScreen的build方法中直接修改控制器文本
    非release模式下,每次build都会执行_emailController.text = ''等代码,不仅清空用户输入,还会触发控制器状态更新,可能引发额外的重建循环。

  3. StatelessWidget持有控制器的风险
    虽然StatelessWidget可以持有TextEditingController,但如果widget实例被频繁替换,控制器的状态会直接丢失,无法维持输入框的焦点和内容。


修复步骤

步骤1:将LoginScreen改为StatefulWidget,迁移控制器初始化逻辑

把控制器的初始化移至initState,确保仅初始化一次,避免因widget重建导致控制器实例丢失:

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  static Route<void> route() {
    return MaterialPageRoute<void>(builder: (_) => const LoginScreen());
  }

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  late final TextEditingController _emailController;
  late final TextEditingController _passwordController;
  late final TextEditingController _selectUrl;

  @override
  void initState() {
    super.initState();
    _emailController = TextEditingController();
    _passwordController = TextEditingController();
    _selectUrl = TextEditingController();

    // 仅在初始化阶段设置调试环境默认值
    if (!kReleaseMode) {
      _emailController.text = '';
      _passwordController.text = '';
      _selectUrl.text = baseUrl;
    }
  }

  @override
  void dispose() {
    // 及时释放控制器资源
    _emailController.dispose();
    _passwordController.dispose();
    _selectUrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Align(
                  alignment: Alignment.center,
                  child: Padding(
                    padding: const EdgeInsets.fromLTRB(20.0),
                    child: Image.asset(
                      'assets/images/logo.png',
                      color: isDarkMode(context)
                          ? Theme.of(context).colorScheme.primary
                          : null,
                    ),
                  ),
                ),
                TextFormField(
                  controller: _emailController,
                  decoration: const InputDecoration(
                    hintText: 'Enter your email',
                  ),
                ),
                TextFormField(
                  controller: _passwordController,
                  decoration: const InputDecoration(
                    hintText: 'Enter your password',
                  ),
                  obscureText: true,
                ),
                BlocConsumer<AuthenticationBloc, AuthenticationState>(
                  listener: (context, state) {
                    if (state is AuthenticationFailureState) {
                      _emailController.text = state.fields['email'] ?? '';
                      _passwordController.text = state.fields['password'] ?? '';
                      showDialog(
                          context: context,
                          builder: (context) {
                            return AlertDialog(
                                title: const Text('Login failed'),
                                content: Text(
                                  state.errorMessage,
                                  textAlign: TextAlign.center,
                                ),
                                icon: const Icon(
                                  Icons.error,
                                  color: Colors.red,
                                ));
                          });
                    } else if (state is AuthenticationSuccessState) {
                      context
                          .read<CalendarDataBloc>()
                          .add(FetchCalendarEvents());

                      context.read<HomeBloc>().add(FetchCheckedInEvents());

                      Navigator.pushReplacementNamed(context, '/home');
                    }
                  },
                  builder: (context, state) {
                    return state is AuthenticationLoadingState &&
                            state.isLoading
                        ? const Center(
                            child: CircularProgressIndicator(),
                          )
                        : SizedBox(
                            child: FilledButton(
                              onPressed: () {
                                BlocProvider.of<AuthenticationBloc>(context)
                                    .add(
                                  LoginUser(
                                    _emailController.text.trim(),
                                    _passwordController.text.trim(),
                                  ),
                                );
                              },
                              child: const Text(
                                'Login',
                                style: TextStyle(
                                  fontSize: 20,
                                ),
                              ),
                            ),
                          );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

步骤2:缓存LoginScreen实例,避免重复创建

在MyApp中缓存LoginScreen的实例,防止每次重建都生成新对象:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const platform = MethodChannel('in.co.ezerx.sales_iq/check');
  // 缓存LoginScreen实例
  final _loginScreen = const LoginScreen();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: getTheme(context, currentTheme: context.watch<ThemeCubit>().state),
      home: Platform.isAndroid
          ? FutureBuilder<bool>(
              future: _isDeveloperOptionsEnabled(),
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Scaffold(
                    body: Center(
                      child: CircularProgressIndicator(),
                    ),
                  );
                } else if (snapshot.hasError) {
                  return const Scaffold(
                    body: Center(
                      child: Text('Error checking developer options'),
                    ),
                  );
                } else if (snapshot.data == true && kReleaseMode) {
                  return const Scaffold(
                    body: Center(
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text('Developer options are enabled'),
                          Text('Please disable developer options to continue'),
                        ],
                      ),
                    ),
                  );
                } else {
                  return BlocBuilder<AuthenticationBloc, AuthenticationState>(
                      builder: (context, state) {
                    if (state is AuthenticationSuccessState) {
                      return const HomeScreen();
                    } else {
                      // 使用缓存的实例
                      return _loginScreen;
                    }
                  });
                }
              })
          : BlocBuilder<AuthenticationBloc, AuthenticationState>(
              builder: (context, state) {
              if (state is AuthenticationSuccessState) {
                return const HomeScreen();
              } else {
                // 使用缓存的实例
                return _loginScreen;
              }
            }),
      onGenerateRoute: (setting) => _generateRoute(setting),
    );
  }

  // 原有_isDeveloperOptionsEnabled和_generateRoute方法保持不变
}

步骤3:缩小主题监听范围,减少MyApp重建

将context.watch<ThemeCubit>()的监听范围限制在MaterialApp内部,避免整个MyApp因主题变化重建:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const platform = MethodChannel('in.co.ezerx.sales_iq/check');
  final _loginScreen = const LoginScreen();

  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (context) {
        final themeMode = context.watch<ThemeCubit>().state;
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          theme: getTheme(context, currentTheme: themeMode),
          // 原有home和路由代码保持不变
        );
      },
    );
  }
}

额外建议

  • 禁止在build方法中修改可变状态(如TextEditingController的text),这类操作应放在initState、didChangeDependencies或用户交互回调中。
  • 对无状态widget使用const构造函数,减少不必要的实例创建。
  • 需持久化状态的widget优先使用StatefulWidget管理状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:54:56