Flutter:点击外部失焦时如何保留TextField输入文本?
问题描述
开发Flutter登录页面时,自定义了有状态的TextInputField组件,用于邮箱和密码输入区域。目前遇到的问题是:点击输入框外部区域使页面失焦时,所有输入框内的文本都会被清空。尝试了多种失焦实现方式,均未解决问题,寻求解决方案。
自定义TextInputField代码
import 'package:flutter/material.dart'; import 'package:flutter_parisa/constants.dart'; class TextInputField extends StatefulWidget { final String labelText; final TextEditingController controller; bool isObscure; final IconData icon; final bool hasSuffixIcon; TextInputField( {super.key, required this.labelText, required this.controller, required this.icon, this.isObscure = false, required this.hasSuffixIcon}); @override State<TextInputField> createState() => _TextInputFieldState(); } class _TextInputFieldState extends State<TextInputField> { final GlobalKey _key = GlobalKey(); void toggleObscure() { setState(() { widget.isObscure = !widget.isObscure; }); } void unFocus() { FocusScope.of(context).unfocus(); } @override Widget build(BuildContext context) { return Form( key: _key, child: TextFormField( onTapOutside: (event) { FocusScope.of(context).unfocus(); }, controller: widget.controller, obscureText: widget.isObscure, decoration: InputDecoration( labelStyle: const TextStyle(color: authbuttonColor), prefixIconColor: authbuttonColor, suffixIcon: widget.hasSuffixIcon ? IconButton( onPressed: () { toggleObscure(); }, icon: widget.isObscure ? const Icon( Icons.visibility, color: authbuttonColor, ) : const Icon( Icons.visibility_off, color: authbuttonColor, )) : null, prefixIcon: Icon(widget.icon), labelText: widget.labelText, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: const BorderSide( color: authbuttonColor, )), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: focusedBorderColor!)), ), ), ); } }
登录页面代码
import 'package:flutter/material.dart'; import 'package:flutter_parisa/constants.dart'; import 'package:flutter_parisa/views/screens/auth/forgotpassword_screen.dart'; import 'package:flutter_parisa/views/screens/auth/signup_screen.dart'; import 'package:flutter_parisa/views/widgets/custom_elevated_button.dart'; import 'package:flutter_parisa/views/widgets/custom_text_button.dart'; import 'package:flutter_parisa/views/widgets/text_input_field.dart'; import 'package:get/get.dart'; class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); @override Widget build(BuildContext context) { TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); return Scaffold( backgroundColor: authBackgroundColor, body: SingleChildScrollView( child: Container( color: authBackgroundColor, child: InkWell( highlightColor: Colors.transparent, hoverColor: Colors.transparent, splashColor: Colors.transparent, onTap: () { //FocusScope.of(context).requestFocus(FocusNode()); // FocusScopeNode currentFocus = FocusScope.of(context); // if (!currentFocus.hasPrimaryFocus) { // currentFocus.unfocus(); // } //FocusScope.of(context).unfocus(); //FocusManager.instance.primaryFocus?.unfocus(); }, child: SafeArea( child: SizedBox( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: Column( children: [ Container( height: MediaQuery.of(context).size.height * 0.3, width: double.infinity, decoration: const BoxDecoration( image: DecorationImage( fit: BoxFit.contain, image: AssetImage('assets/images/parisa_logo.png'))), ), const SizedBox( height: 40, width: double.infinity, child: Padding( padding: EdgeInsets.only(left: 25), child: Text( 'Daxil ol', style: TextStyle( color: Colors.black, fontSize: 28, fontWeight: FontWeight.bold), ), ), ), //here we have to add custom textfield Padding( padding: const EdgeInsets.symmetric( horizontal: 25, vertical: 5), child: Column( children: [ TextInputField( hasSuffixIcon: false, labelText: 'Email', controller: emailController, icon: Icons.mail), const SizedBox( height: 15, ), TextInputField( hasSuffixIcon: true, labelText: 'Şifrə', controller: passwordController, icon: Icons.lock, isObscure: true), ], ), ), Container( width: double.infinity, alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 25, bottom: 10), child: CustomTextButton( textColor: authbuttonColor, buttonText: 'Şifrəni Unutduz?', fontSize: 14, isBold: true, onPressed: () { Get.to(() => ForgotPassword(emailContent: emailController)); }), ), Obx(() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 25), child: authController.isLoading.value ? const Center( child: CircularProgressIndicator( color: authbuttonColor, ), ) : CustomElevatedButton( fontSize: 24, buttonColor: authbuttonColor, buttonName: 'Daxil ol', onPressed: () { FocusScope.of(context).unfocus(); authController.loginUser( emailController.text.trim(), passwordController.text.trim()); }, textColor: Colors.white, ), ); }), SizedBox( height: MediaQuery.of(context).size.height * 0.1, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Hesabınız yoxdur?', style: TextStyle( color: Colors.grey, fontSize: 14, fontWeight: FontWeight.bold), ), CustomTextButton( textColor: authbuttonColor, buttonText: 'Qeydiyyat', fontSize: 14, isBold: true, onPressed: () => Get.to(() => const SignUpScreen())), ], ), ), ], ), ), ), ), ), ), ); } }
尝试过的失焦方式
//FocusScope.of(context).requestFocus(FocusNode()); // FocusScopeNode currentFocus = FocusScope.of(context); // if (!currentFocus.hasPrimaryFocus) { // currentFocus.unfocus(); // } //FocusScope.of(context).unfocus(); //FocusManager.instance.primaryFocus?.unfocus();
解决方案
核心问题分析
输入文本清空的根本原因不是失焦方式的问题,而是LoginScreen作为StatelessWidget,将TextEditingController的实例化放在了build方法内部。Flutter中StatelessWidget的build方法会在多种场景下重新执行(比如组件状态变化、布局更新、失焦触发的重绘等),每次执行都会创建新的TextEditingController实例,导致之前输入的文本丢失。
另外,自定义TextInputField中每个TextFormField单独包裹Form组件是不必要的,Form通常用于多个输入框的统一验证和状态管理,单个输入框无需嵌套Form,这也可能引发不必要的状态重置。
具体修复步骤
- 调整
LoginScreen中控制器的生命周期
将emailController和passwordController从build方法中移到类的成员变量位置,确保控制器实例在组件生命周期内保持唯一:
class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); // 将控制器移到类成员变量 final TextEditingController emailController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); @override Widget build(BuildContext context) { // 移除原build内的控制器实例化代码 return Scaffold( // 原有代码保持不变 ); } }
- 移除
TextInputField中的冗余Form组件
修改_TextInputFieldState的build方法,去掉外层的Form:
@override Widget build(BuildContext context) { return TextFormField( onTapOutside: (event) { FocusScope.of(context).unfocus(); }, controller: widget.controller, obscureText: widget.isObscure, decoration: InputDecoration( // 原有装饰代码保持不变 ), ); }
- 启用失焦逻辑
在LoginScreen的InkWell的onTap中添加正确的失焦代码:
onTap: () { FocusManager.instance.primaryFocus?.unfocus(); },
额外优化建议
如果使用GetX状态管理,可以将TextEditingController放在GetX的控制器中管理,进一步提升状态的稳定性和可维护性。
内容的提问来源于stack exchange,提问作者Farid Ahmadov
相关产品推荐
相关产品推荐

