Android学Flutter学习笔记 第四节 Android视角认知Flutter(input)

input

在Android中,我们一般使用EditText来获取用户的输入。

Flutter 提供了两种文本字段:TextField 和 TextFormField。

TextField

TextField 是最常用的文本输入组件。

默认情况下,TextField 会带有下划线装饰。你可以通过为 TextField 的 decoration 属性提供 InputDecoration 来添加标签、图标、行内提示文本和错误文本。要完全移除装饰(包括下划线和为标签预留的空间),请将 decoration 设置为 null。

下面是常见的使用案例:

kotlin 复制代码
 children: [
            TextField(
              decoration: InputDecoration(
                hintText: '请输入',
                label: Text('用户名'),
                icon: Icon(Icons.person, size: 40),
              ),
            ),

            TextField(
              decoration: InputDecoration(
                hintText: '请输入',
                border: InputBorder.none, // 去掉下划线
              ),
            ),
            TextField(decoration: null),
            ]

TextFormField

TextFormField 包装了一个 TextField 并将其与周围的 Form 集成。这提供了额外的功能,例如验证以及与其他 FormField 小部件的集成。

与TextField相比较,TextFormField的核心是与form联动、输入校验。

下面是一个登录案例:

kotlin 复制代码
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              // 邮箱输入
              TextFormField(
                decoration: InputDecoration(
                  labelText: '邮箱',
                  hintText: 'example@email.com',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入邮箱';
                  }
                  // 更严谨的邮箱正则
                  final emailRegex = RegExp(
                    r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
                  );
                  if (!emailRegex.hasMatch(value)) {
                    return '请输入有效的邮箱地址';
                  }
                  return null;
                },
                onSaved: (value) {
                  _email = value ?? '';
                },
              ),

              SizedBox(height: 20),

              // 密码输入
              TextFormField(
                decoration: InputDecoration(
                  labelText: '密码',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入密码';
                  }
                  if (value.length < 6) {
                    return '密码至少6位字符';
                  }
                  if (!value.contains(RegExp(r'[A-Z]'))) {
                    return '密码必须包含大写字母';
                  }
                  if (!value.contains(RegExp(r'[0-9]'))) {
                    return '密码必须包含数字';
                  }
                  return null;
                },
                onSaved: (value) {
                  _password = value ?? '';
                },
              ),

              SizedBox(height: 30),

              // 提交按钮
              ElevatedButton(
                onPressed: _submitForm,
                style: ElevatedButton.styleFrom(
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('登录', style: TextStyle(fontSize: 18)),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _submitForm() {
    // 1. 触发所有验证
    if (_formKey.currentState!.validate()) {
      print('✅ 所有验证通过');

      // 2. 保存表单数据
      _formKey.currentState!.save();

      // 3. 处理数据
      print('邮箱: $_email');
      print('密码: $_password');

      // 4. 执行登录逻辑
      _performLogin(_email, _password);
    } else {
      Fluttertoast.showToast(msg: '❌ 表单验证失败');
    }
  }

  void _performLogin(String email, String password) {
    // 实际登录逻辑
    Fluttertoast.showToast(msg: '登录成功');
  }
}

获取TextField的值

1. Create a TextEditingController

要获取用户在文本字段中输入的文本,需创建一个 TextEditingController 并将其提供给 TextField 或 TextFormField。

重要:使用完 TextEditingController 后,请调用其 dispose 方法。这能确保释放该对象所使用的所有资源。

kotlin 复制代码
// Define a custom Form widget.
class MyCustomForm extends StatefulWidget {
  const MyCustomForm({super.key});

  @override
  State<MyCustomForm> createState() => _MyCustomFormState();
}

// Define a corresponding State class.
// This class holds the data related to the Form.
class _MyCustomFormState extends State<MyCustomForm> {
  // Create a text controller and use it to retrieve the current value
  // of the TextField.
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when the widget is disposed.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next step.
  }
}
2. Supply the TextEditingController to a TextField

既然你已经有了一个 TextEditingController,那就通过 controller 属性将它与文本字段连接起来:

kotlin 复制代码
return TextField(controller: myController);
3. Display the current value of the text field

向文本字段提供 TextEditingController 后,就可以开始读取值了。使用 TextEditingController 提供的 text 属性来获取用户在文本字段中输入的字符串。

以下代码会在用户点击悬浮操作按钮时,显示一个包含文本字段当前值的警告对话框。

kotlin 复制代码
FloatingActionButton(
  // When the user presses the button, show an alert dialog containing
  // the text that the user has entered into the text field.
  onPressed: () {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          // Retrieve the text that the user has entered by using the
          // TextEditingController.
          content: Text(myController.text),
        );
      },
    );
  },
  tooltip: 'Show me the value!',
  child: const Icon(Icons.text_fields),
),

实时处理文本的变更

在某些情况下,每当文本字段中的文本发生变化时运行一个回调函数是很有用的。例如,你可能想要构建一个带有自动完成功能的搜索界面,在用户输入时更新结果。

每次文本发生变化时,如何运行回调函数?使用 Flutter,你有两个选择:

1、为 TextField 或 TextFormField 提供一个 onChanged () 回调函数。

2、使用TextEditingController.

向 TextField 或 TextFormField 提供一个 onChanged () 回调函数

最简单的方法是向 TextField 或 TextFormField 提供一个 onChanged () 回调函数。每当文本发生变化时,就会调用该回调函数。

在这个示例中,每当文本发生变化时,就将文本字段的当前值及其长度打印到控制台。

处理用户输入时,使用字符很重要,因为文本可能包含复杂字符。这样可以确保每个字符都能按照用户看到的样子被正确计数。

kotlin 复制代码
TextField(
  onChanged: (text) {
    print('First text field: $text (${text.characters.length})');
  },
),
使用TextEditingController

一种更强大但也更复杂的方法是,提供一个 TextEditingController 作为 TextField 或 TextFormField 的 controller 属性。

若要在文本更改时收到通知,请按照以下步骤使用 addListener () 方法监听控制器:

1.创建一个 TextEditingController。

kotlin 复制代码
// Define a custom Form widget.
class MyCustomForm extends StatefulWidget {
  const MyCustomForm({super.key});

  @override
  State<MyCustomForm> createState() => _MyCustomFormState();
}

// Define a corresponding State class.
// This class holds data related to the Form.
class _MyCustomFormState extends State<MyCustomForm> {
  // Create a text controller. Later, use it to retrieve the
  // current value of the TextField.
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when the widget is removed from the
    // widget tree.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next step.
  }
}

请记住,当 TextEditingController 不再需要时,要对其进行销毁。这能确保释放该对象所使用的所有资源。

2.将 TextEditingController 连接到文本字段。

将 TextEditingController 提供给 TextField 或 TextFormField。一旦将这两个类连接起来,您就可以开始监听文本字段的变化了。

kotlin 复制代码
TextField(controller: myController),

3.创建一个函数来打印最新值。

你需要一个在文本每次变化时运行的函数。在_MyCustomFormState 类中创建一个方法,用于打印出文本字段的当前值。

kotlin 复制代码
void _printLatestValue() {
  final text = myController.text;
  print('Second text field: $text (${text.characters.length})');
}

4.监听控制器的变化。

最后,监听 TextEditingController,并在文本发生变化时调用_printLatestValue () 方法。为此,请使用 addListener () 方法。

在_MyCustomFormState 类初始化时开始监听变化,在_MyCustomFormState 被销毁时停止监听。

kotlin 复制代码
@override
void initState() {
  super.initState();

  // Start listening to changes.
  myController.addListener(_printLatestValue);
}
kotlin 复制代码
@override
void dispose() {
  // Clean up the controller when the widget is removed from the widget tree.
  // This also removes the _printLatestValue listener.
  myController.dispose();
  super.dispose();
}

Focus

当一个文本框被选中并接受输入时,就说它处于 "焦点" 状态。通常,用户通过点击将焦点切换到文本框,而开发者则通过使用本教程中描述的工具,以编程方式将焦点切换到文本框。

管理焦点是创建具有直观流程的表单的基本工具。例如,假设你有一个带有文本字段的搜索屏幕。当用户导航到搜索屏幕时,你可以将焦点设置到用于输入搜索词的文本字段上。这样,用户在屏幕一显示时就能开始输入,无需手动点击该文本字段。

接下来,了解如何在文本字段一显示时就使其获得焦点,以及如何在点击按钮时让文本字段获得焦点。

可见就立即聚焦

要在文本字段一显示就使其获得焦点,请使用 autofocus 属性。

kotlin 复制代码
TextField(
  autofocus: true,
);
点击按钮时聚焦文本字段

你可能需要在稍后的某个时间点将焦点赋予文本字段。在实际情况中,你可能还需要响应 API 调用或验证错误,将焦点赋予特定的文本字段。在本示例中,按照以下步骤在用户按下按钮后将焦点赋予文本字段:

  1. Create a FocusNode

首先,创建一个 FocusNode。使用 FocusNode 在 Flutter 的 "焦点树" 中标识特定的 TextField。这使你能够在后续步骤中为该 TextField 设置焦点。

由于焦点节点是长期存在的对象,因此请使用 State 对象来管理其生命周期。按照以下说明,在 State 类的 initState () 方法中创建 FocusNode 实例,并在 dispose () 方法中对其进行清理:

kotlin 复制代码
// Define a custom Form widget.
class MyCustomForm extends StatefulWidget {
  const MyCustomForm({super.key});

  @override
  State<MyCustomForm> createState() => _MyCustomFormState();
}

// Define a corresponding State class.
// This class holds data related to the form.
class _MyCustomFormState extends State<MyCustomForm> {
  // Define the focus node. To manage the lifecycle, create the FocusNode in
  // the initState method, and clean it up in the dispose method.
  late FocusNode myFocusNode;

  @override
  void initState() {
    super.initState();

    myFocusNode = FocusNode();
  }

  @override
  void dispose() {
    // Clean up the focus node when the Form is disposed.
    myFocusNode.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next step.
  }
}
  1. Pass the FocusNode to a TextField
    既然你已经有了一个 FocusNode,就在 build () 方法中将它传递给特定的 TextField 吧。
kotlin 复制代码
@override
Widget build(BuildContext context) {
  return TextField(focusNode: myFocusNode);
}
  1. Give focus to the TextField when a button is tapped

最后,当用户点击悬浮操作按钮时,让文本字段获得焦点。使用 requestFocus () 方法来完成这项任务。

kotlin 复制代码
FloatingActionButton(
  // When the button is pressed,
  // give focus to the text field using myFocusNode.
  onPressed: () => myFocusNode.requestFocus(),
),

最后附上本期全部的测试代码

kotlin 复制代码
import 'package:flutter/material.dart';

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

  @override
  State<InputPage> createState() => _InputPageState();
}

class _InputPageState extends State<InputPage> {
  final textController = TextEditingController();
  var inputText = '';
  late FocusNode myFocusNode;
  late bool hasFocus = false;
  @override
  void initState() {
    super.initState();
    myFocusNode = FocusNode();
    myFocusNode.addListener(() {
      setState(() {
        hasFocus = myFocusNode.hasFocus;
      });
    });
    textController.addListener(() {
      setState(() {
        inputText = (textController.text);
      });
    });
  }

  @override
  dispose() {
    textController.dispose();
    myFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('input test')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              autofocus: true, //可见就立刻聚焦
              decoration: InputDecoration(
                hintText: '请输入',
                label: Text('用户名'),
                icon: Icon(Icons.person, size: 40),
              ),
            ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    focusNode: myFocusNode,
                    decoration: InputDecoration(
                      hintText: '请输入',
                      border: InputBorder.none, // 去掉下划线
                    ),
                  ),
                ),
                GestureDetector(
                  onTap: () {
                    if (hasFocus) {
                      myFocusNode.unfocus();
                      //这一部分实测,Android可用,web取消后立马会自动获得
                    } else {
                      myFocusNode.requestFocus();
                    }
                  },
                  child: Text(hasFocus ? '取消' : '聚焦'),
                ),
              ],
            ),

            TextField(decoration: null),
            TextFormField(
              decoration: InputDecoration(
                labelText: '邮箱',
                prefixIcon: Icon(Icons.email),
              ),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入邮箱';
                }
                if (!RegExp(
                  r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
                ).hasMatch(value)) {
                  return '邮箱格式不正确';
                }
                return null;
              },
            ),
            TextField(
              controller: textController,
              decoration: InputDecoration(hintText: '请输入'),
            ),
            Text(inputText, style: TextStyle(fontSize: 20, color: Colors.red)),
          ],
        ),
      ),
    );
  }
}
kotlin 复制代码
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              // 邮箱输入
              TextFormField(
                decoration: InputDecoration(
                  labelText: '邮箱',
                  hintText: 'example@email.com',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入邮箱';
                  }
                  // 更严谨的邮箱正则
                  final emailRegex = RegExp(
                    r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
                  );
                  if (!emailRegex.hasMatch(value)) {
                    return '请输入有效的邮箱地址';
                  }
                  return null;
                },
                onSaved: (value) {
                  _email = value ?? '';
                },
              ),

              SizedBox(height: 20),

              // 密码输入
              TextFormField(
                decoration: InputDecoration(
                  labelText: '密码',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入密码';
                  }
                  if (value.length < 6) {
                    return '密码至少6位字符';
                  }
                  if (!value.contains(RegExp(r'[A-Z]'))) {
                    return '密码必须包含大写字母';
                  }
                  if (!value.contains(RegExp(r'[0-9]'))) {
                    return '密码必须包含数字';
                  }
                  return null;
                },
                onSaved: (value) {
                  _password = value ?? '';
                },
              ),

              SizedBox(height: 30),

              // 提交按钮
              ElevatedButton(
                onPressed: _submitForm,
                style: ElevatedButton.styleFrom(
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('登录', style: TextStyle(fontSize: 18)),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _submitForm() {
    // 1. 触发所有验证
    if (_formKey.currentState!.validate()) {
      print('✅ 所有验证通过');

      // 2. 保存表单数据
      _formKey.currentState!.save();

      // 3. 处理数据
      print('邮箱: $_email');
      print('密码: $_password');

      // 4. 执行登录逻辑
      _performLogin(_email, _password);
    } else {
      Fluttertoast.showToast(msg: '❌ 表单验证失败');
    }
  }

  void _performLogin(String email, String password) {
    // 实际登录逻辑
    Fluttertoast.showToast(msg: '登录成功');
  }
}
相关推荐
智慧化智能化数字化方案13 小时前
【精品资料鉴赏】财务数智化智能化建设学习
人工智能·学习·财务数字化·财务数智化·财务一体化·财务共享平台·财务成熟度评估模型
lxysbly13 小时前
nes模拟器安卓版下载汉化版2026
android
烟囱土著13 小时前
捣鼓15天,我写了一个拼音发音点读小程序
学习·微信·微信小程序·小程序·拼音
YIN_尹14 小时前
【MySQL】库的操作
android·数据库·mysql
_Kayo_14 小时前
node.js 学习笔记5
笔记·学习
报错小能手14 小时前
线程池学习(四)实现缓存线程池(Cached ThreadPool)
java·学习·缓存
2501_9160088914 小时前
不连 Xcode,也能把 iPhone App 的实时日志看清楚
android·ios·小程序·https·uni-app·iphone·webview
叶辞树14 小时前
安卓的开机动画和FallbackHome机制
android
窗边鸟14 小时前
小白日记之二维数组(java学习)
java·开发语言·学习
jzlhll12314 小时前
PictureSelector android的发展和我修改全选功能
android