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

Android模拟器中Flutter表单与TextField异常问题求助

Flutter登录页Android模拟器输入异常排查与修复

问题描述

开发的Flutter登录页面支持学号、密码输入及登录逻辑,Chrome浏览器运行正常,但在Android模拟器中进入页面即出现大量日志输出,弹出软键盘后输入/删除字符时错误频发。

错误日志分析

给出的日志中,W/WindowOnBackDispatcher是返回键回调的警告,可通过在AndroidManifest.xml中添加android:enableOnBackInvokedCallback="true"解决,但这不是输入异常的核心原因。其余D/、I/开头的日志多为系统调试信息(软键盘显示/隐藏、输入连接状态、EGL性能统计),未直接显示崩溃栈,但频繁的软键盘请求取消(onCancelled at PHASE_CLIENT_APPLY_ANIMATION)提示焦点管理存在冲突。

代码问题排查

  1. Focus组件与TextField焦点冲突:使用独立的Focus组件包裹TextField,而TextField本身内置焦点管理逻辑,双重焦点监听会导致Android端软键盘焦点混乱,触发频繁的显示/取消请求。
  2. 未释放资源:TextEditingController未在dispose中释放,可能引发内存泄漏和状态异常。
  3. 焦点切换无逻辑:缺少输入框间的焦点切换处理,加重焦点管理负担的同时影响用户体验。

修复后的代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 14:50:07