Android模拟器中Flutter表单与TextField异常问题求助
Flutter登录页Android模拟器输入异常排查与修复
问题描述
开发的Flutter登录页面支持学号、密码输入及登录逻辑,Chrome浏览器运行正常,但在Android模拟器中进入页面即出现大量日志输出,弹出软键盘后输入/删除字符时错误频发。
错误日志分析
给出的日志中,W/WindowOnBackDispatcher是返回键回调的警告,可通过在AndroidManifest.xml中添加android:enableOnBackInvokedCallback="true"解决,但这不是输入异常的核心原因。其余D/、I/开头的日志多为系统调试信息(软键盘显示/隐藏、输入连接状态、EGL性能统计),未直接显示崩溃栈,但频繁的软键盘请求取消(onCancelled at PHASE_CLIENT_APPLY_ANIMATION)提示焦点管理存在冲突。
代码问题排查
- Focus组件与TextField焦点冲突:使用独立的
Focus组件包裹TextField,而TextField本身内置焦点管理逻辑,双重焦点监听会导致Android端软键盘焦点混乱,触发频繁的显示/取消请求。 - 未释放资源:
TextEditingController未在dispose中释放,可能引发内存泄漏和状态异常。 - 焦点切换无逻辑:缺少输入框间的焦点切换处理,加重焦点管理负担的同时影响用户体验。
修复后的代码
import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:wolfpackapp/models_services/teachassist_model.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final FocusNode _usernameFocusNode = FocusNode(); final FocusNode _passwordFocusNode = FocusNode(); @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _usernameFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, extendBody: true, body: SingleChildScrollView( child: Column( children: [ SizedBox( height: 350, child: Padding( padding: const EdgeInsets.all(50), child: Column( children: [ UsernameField( controller: _usernameController, focusNode: _usernameFocusNode, nextFocusNode: _passwordFocusNode, ), PasswordField( controller: _passwordController, focusNode: _passwordFocusNode, ), ], ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 50), child: SizedBox( width: double.infinity, child: ElevatedButton( style: Theme.of(context).elevatedButtonTheme.style, child: Padding( padding: const EdgeInsets.all(15), child: Text( 'Sign In', style: GoogleFonts.lato( color: Theme.of(context).colorScheme.inversePrimary, fontSize: 20, fontWeight: FontWeight.w600, ), ), ), onPressed: () { String username = _usernameController.text.trim(); String password = _passwordController.text.trim(); authorizeUser(username, password); }, ), ), ), ], ), ), ); } } class UsernameField extends StatefulWidget { final TextEditingController controller; final FocusNode focusNode; final FocusNode nextFocusNode; const UsernameField({ required this.controller, required this.focusNode, required this.nextFocusNode, super.key, }); @override State<UsernameField> createState() => _UsernameFieldState(); } class _UsernameFieldState extends State<UsernameField> { late Color _colorText; final defaultColor = Colors.grey; @override void initState() { super.initState(); _colorText = defaultColor; widget.focusNode.addListener(_onFocusChange); } @override void dispose() { widget.focusNode.removeListener(_onFocusChange); super.dispose(); } void _onFocusChange() { setState(() { _colorText = widget.focusNode.hasFocus ? Theme.of(context).colorScheme.secondary : defaultColor; }); } @override Widget build(BuildContext context) { return TextField( controller: widget.controller, focusNode: widget.focusNode, textInputAction: TextInputAction.next, onSubmitted: (_) { FocusScope.of(context).requestFocus(widget.nextFocusNode); }, decoration: InputDecoration( prefixIcon: Icon(Icons.person, color: _colorText), labelText: "Student Number", labelStyle: TextStyle(color: _colorText), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: _colorText), ), ), ); } } class PasswordField extends StatefulWidget { final TextEditingController controller; final FocusNode focusNode; const PasswordField({ required this.controller, required this.focusNode, super.key, }); @override State<PasswordField> createState() => _PasswordFieldState(); } class _PasswordFieldState extends State<PasswordField> { late Color _colorText; final defaultColor = Colors.grey; @override void initState() { super.initState(); _colorText = defaultColor; widget.focusNode.addListener(_onFocusChange); } @override void dispose() { widget.focusNode.removeListener(_onFocusChange); super.dispose(); } void _onFocusChange() { setState(() { _colorText = widget.focusNode.hasFocus ? Theme.of(context).colorScheme.secondary : defaultColor; }); } @override Widget build(BuildContext context) { return TextField( controller: widget.controller, focusNode: widget.focusNode, obscureText: true, textInputAction: TextInputAction.done, onSubmitted: (_) { FocusScope.of(context).unfocus(); String username = _usernameController.text.trim(); String password = _passwordController.text.trim(); authorizeUser(username, password); }, decoration: InputDecoration( prefixIcon: Icon(Icons.lock, color: _colorText), labelText: "Password", labelStyle: TextStyle(color: _colorText), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: _colorText), ), ), ); } }
关键修改点
- 移除
Focus组件,改用TextField自带的focusNode监听焦点变化,彻底解决焦点冲突问题。 - 新增
dispose方法释放TextEditingController和FocusNode,避免内存泄漏。 - 添加输入框间的焦点切换逻辑(用户名回车跳密码框,密码回车触发登录),优化体验的同时减少焦点异常触发。
- 优化颜色状态初始化与更新逻辑,确保UI状态准确同步。
内容的提问来源于stack exchange,提问作者ThrillerGriller
相关产品推荐
相关产品推荐

