点击TextFormField时键盘反复开合,Widget多次重建问题
核心问题
点击TextFormField时键盘弹出后立刻关闭,根源在于LoginScreen被频繁重建,导致输入框控制器实例被替换,焦点丢失。具体诱因如下:
MyApp重建时会创建全新的LoginScreen实例
MyApp中使用context.watch<ThemeCubit>()监听主题变化,每次主题切换都会触发MyApp重建;同时Android平台的FutureBuilder完成、AuthenticationBloc状态变更时,BlocBuilder也会重建。每次重建都会返回LoginScreen(),生成全新的LoginScreen对象,原有的TextEditingController被销毁,输入框失去焦点。LoginScreen的build方法中直接修改控制器文本
非release模式下,每次build都会执行_emailController.text = ''等代码,不仅清空用户输入,还会触发控制器状态更新,可能引发额外的重建循环。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

