4.3 常用 UI 组件实战:按钮、输入框、列表


4.3 常用 UI 组件实战:按钮、输入框、列表

本节摘要:本节把两套框架的常用组件放到同一个真实需求里对照实战——做一个"输入文字、点按钮、加入列表"的完整界面。RN 用 Button 与 TouchableOpacity 做按钮、TextInput 做输入、FlatList 做列表;Flutter 用 ElevatedButton 做按钮、TextField 做输入、ListView 做列表。同一需求、两种解法,正是双框架学习的核心训练。

上手前先明确

阅读完本节,你应当能够:

  1. 在 RN 中实现按钮、输入框与列表的组合界面。
  2. 在 Flutter 中实现同样的界面。
  3. 对比两套框架在组件 API 上的异同。
  4. 理解受控输入(RN)与控制器输入(Flutter)的机制差异。

一、问题与直觉

理论讲完总要落地。本节选一个所有 App 都会遇到的需求:待办输入——输入框里打字、点按钮、内容进列表。这个需求麻雀虽小,五脏俱全:它同时用到输入组件(状态)、按钮组件(交互)、列表组件(渲染),是检验"组件体系是否真正掌握"的最佳试金石。

做这个练习最大的价值不在"能写出代码",而在对照。同一个需求,你会看到 RN 与 Flutter 各自如何处理输入、按钮与列表,也会看到它们面对同类问题时的设计取向差异。带着"同一需求两种解法"的眼光来读,收获远超"学会某个组件"。

顺便说一句,这个练习也是后续章节的常驻示例。第 5 章讲状态管理、第 6 章讲调试与原生能力,都会拿"待办"类应用举例。现在把基础版本写扎实,后面每章都能在它上面加新能力,形成一条连续的实践主线——这比每章从零开新项目高效得多。如果你还没动手写,强烈建议现在就照着下文把代码敲一遍。

二、核心原理

2.1 RN 侧:待办输入界面

RN 的输入框是受控组件——value 来自状态、onChangeText 更新状态,界面显示的永远等于状态值。按钮用 Button(系统样式)或 TouchableOpacity(自定义样式)。列表用 FlatList。

import React, { useState } from 'react'; import { View, TextInput, TouchableOpacity, Text, FlatList, StyleSheet } from 'react-native'; function TodoScreen() { const [text, setText] = useState(''); const [items, setItems] = useState([]); const addItem = () => { if (!text.trim()) return; setItems([...items, { id: String(Date.now()), text }]); setText(''); }; return ( <View style={styles.container}> <TextInput style={styles.input} placeholder="输入待办" value={text} onChangeText={setText} /> <TouchableOpacity style={styles.button} onPress={addItem}> <Text style={styles.buttonText}>添加</Text> </TouchableOpacity> <FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => <Text style={styles.item}>{item.text}</Text>} /> </View> ); }

拆解这段代码的三个关键点:受控输入——TextInput 的 value 与 onChangeText 构成闭环;不可变更新——setItems 用展开运算符产生新数组,而不是原地 push;FlatList 三要素——data 提供数据、keyExtractor 提供唯一键、renderItem 描述每行渲染。

展开说说"不可变更新"这一点,它是 RN 状态管理的基石逻辑:React 靠引用比较判断状态是否变化,原地修改数组(items.push)不会产生新引用,界面自然不更新。用展开运算符 [...items, newItem] 创建新数组,React 才能感知变化并触发重渲染。这条规则到第 5 章讲 Redux、Zustand 时还会反复出现,是贯穿整个状态管理的核心习惯。现在就在这个最小例子里养成它,后面会少踩很多坑。

2.2 Flutter 侧:同样的待办输入

Flutter 的输入框用 TextEditingController 管理内容,按钮用 ElevatedButton,列表用 ListView。差异点在于:Flutter 的输入不是"每次按键更新状态",而是"控制器持有内容,读取时取值"。

import 'package:flutter/material.dart'; class TodoScreen extends StatefulWidget { const TodoScreen({super.key}); @override State<TodoScreen> createState() => _TodoScreenState(); } class _TodoScreenState extends State<TodoScreen> { final TextEditingController _controller = TextEditingController(); final List<String> _items = []; void _addItem() { final text = _controller.text.trim(); if (text.isEmpty) return; setState(() { _items.add(text); _controller.clear(); }); } @override Widget build(BuildContext context) { return Column( children: [ TextField( controller: _controller, decoration: const InputDecoration(hintText: '输入待办'), ), ElevatedButton( onPressed: _addItem, child: const Text('添加'), ), Expanded( child: ListView.builder( itemCount: _items.length, itemBuilder: (context, index) => ListTile(title: Text(_items[index])), ), ), ], ); } }

对应关系一目了然:TextInput 对应 TextField 加 controller,Button 对应 ElevatedButton,FlatList 对应 ListView.builder。这套"组件映射"就是双框架迁移的基本功。

2.2.1 按钮的三种风格与定制选择

按钮在 Flutter 里不止一种,按用途选型是基本功。常见的是三类:

按钮类型 适用场景 视觉特点
ElevatedButton 主要操作,最常用 带底色,视觉突出
TextButton 次要操作 纯文字,低调
OutlinedButton 中等优先级 描边按钮

RN 侧的选择逻辑类似:系统 Button 简单但定制性差,需要自定义样式(背景色、圆角、按压效果)时换 TouchableOpacity。实践建议:需要标准按钮直接用系统组件,需要品牌化定制就换可定制组件。别一开始就陷入"自定义一切"的泥潭——先把标准组件用顺,再按需定制。

2.2.2 ListTile 与 FlatList 的列表项组织

Flutter 的 ListView.builder 里通常配 ListTile 渲染列表项,它自带标题、副标题、前导图标、尾部操作的排版能力。RN 的 FlatList 需要自己写 renderItem 定义行的布局。两者思路不同:Flutter 把"列表项该长什么样"封装进了 ListTile,RN 则把组合自由交给开发者。没有优劣,只有取向——Flutter 偏"开箱即用",RN 偏"灵活组合"。

2.3 两种输入机制的差异

RN 的受控输入是"状态驱动"的:每敲一个字母,onChangeText 更新状态,界面从状态取值。Flutter 的控制器输入是"引用持有"的:TextField 内部持有内容,需要时才从 controller 取值。

两种机制各有取舍。RN 的受控输入让"界面值永远等于状态值",便于状态统一管理;Flutter 的控制器更直接,但状态管理需要额外手段(这也是第 5 章 Flutter 侧状态管理的背景)。理解差异而非评判优劣,是双框架学习应有的态度。

2.4 列表性能:虚拟渲染的真相

FlatList 与 ListView.builder 都采用虚拟渲染:只构建可视区域附近的条目,而不是一次渲染全部。这解释了为什么几千条数据也能流畅滚动。新手常见误区是"我数据才几十条,用 ScrollView 就行"——几十条确实没问题,但一旦数据量涨到几百上千,一次性渲染会明显卡顿。选择原则:内容数量不确定、可能增长,就用虚拟列表组件。这是长期维护项目的重要习惯。

2.4.1 让列表真正可用:空状态与加载

真实列表还有两个绕不开的状态:数据为空时、加载数据时。RN 的 FlatList 提供 ListEmptyComponent 渲染空状态提示;Flutter 里通常在 ListView 外套一层条件判断,或用 FutureBuilder 处理异步加载。这两个状态看似不起眼,却是"能跑的 Demo"与"能上线的页面"之间的分水岭。做本节练习时,建议把空状态提示也加上——"暂无待办,添加一个吧"这句文案,让界面从玩具变成产品。

三、工程实践要点

3.1 组件对照速查表

需求 React Native Flutter 注意点
按钮 Button / TouchableOpacity ElevatedButton Button 定制性弱,自定义用 Touchable
输入框 TextInput(受控) TextField(控制器) 输入机制不同
列表 FlatList ListView.builder 都是虚拟渲染
列表项 renderItem 函数 itemBuilder 函数 概念相同
点击反馈 TouchableOpacity InkWell RN 靠透明度,Flutter 靠水波纹

这张表建议截图或抄下来贴在案头。它不仅是"组件对照表",更是你在两套框架间切换时的索引:看到一个需求,先在表里找到两边对应的组件,再看各自的专属细节。随着你积累的组件越来越多,这张表可以自己往下加行——比如图片选择、日期选择、下拉刷新。亲手扩充这张表的过程,本身就是对双框架知识体系最好的巩固。

3.2 各框架的高频坑

RN 侧:FlatList 的 key 必须唯一且稳定,用数组下标做 key 会导致删除项时渲染错乱;TextInput 忘记受控会出现"输入了但不更新"的诡异问题。

Flutter 侧:忘记释放 TextEditingController 会泄漏内存,用完要在 dispose 里清理;ListView.builder 的 itemCount 必须与数据长度一致。

双框架共通:键盘遮挡输入框、列表滚动与输入冲突、中文输入法的合成文本问题——这些移动端输入场景的坑,两套框架都存在,只是解决方式略有不同。遇到时先搜索"输入框被键盘遮挡"这类关键词,多数有现成方案,不必自己发明轮子。

⚠️ 常见坑(RN):把数组下标当 FlatList 的 key。列表增删时下标变化,框架会误判"哪个条目变了",导致渲染错乱。用每条数据独有的 id 作 key 才是正解。
⚠️ 常见坑(Flutter):忘记在 dispose 里释放 TextEditingController。控制器持有资源,不释放会内存泄漏,长时间运行的应用尤其明显。
💡 关键直觉:看到"列表 + 输入 + 按钮"的组合需求,先想清楚"数据从哪来、存哪里、怎么更新"——三个问题的答案落定,组件代码只是翻译。

3.3 练习进阶:从添加待办到管理待办

把基础功能跑通后,加三个进阶点:点击列表项标记完成(涉及状态更新与样式变化)、删除项(涉及列表可变与 key 稳定)、统计已完成数量(涉及派生数据)。这三个点分别对应第 5 章状态管理的核心主题,现在做一遍,第 5 章会轻松很多。记住:功能越复杂,越要用状态统一驱动界面,别把更新逻辑散落在各个回调里。

FAQ:常用组件的常见疑问

问:RN 里 Button 和 TouchableOpacity 到底怎么选? 需要系统原生按钮、无需定制用 Button;需要自定义外观(背景色、圆角、按压动效)用 TouchableOpacity 或 Pressable。一个经验:绝大多数真实产品的按钮都是自定义外观的,所以 TouchableOpacity 反而是日常主力。新版 RN 还提供了 Pressable,它把按下、悬停、长按等状态统一起来,比 TouchableOpacity 更灵活,值得优先学习。

问:Flutter 的 TextField 为什么用控制器而不是状态? 因为控制器更直接地持有文本内容,读取、清空、设置光标都方便。它与 RN 受控输入的差异来自框架设计取向,不是优劣之分。需要响应输入内容变化时,Flutter 用 onChanged 回调;需要统一状态管理时再引入第 5 章的方案。

问:列表数据变了但界面没更新? RN 侧先检查是否用了不可变更新(新数组替换旧数组);Flutter 侧检查是否包了 setState。两个框架都靠"变化通知"触发重建,原地修改数据而不通知框架,界面自然不会变。这条也是第 5 章会反复强调的核心。

问:列表性能什么时候需要真正优化? 数据超过几百条、或列表项渲染较重(图片、复杂布局)时,需要关注。优化手段从易到难:用虚拟列表组件(FlatList 默认就是)、给列表项加稳定 key、避免列表项内的重复计算。多数场景前两步就够用。

3.4 组件状态归属的一个提醒

待办输入这个例子引出一个状态设计的问题:输入框内容、列表数据分别属于谁。按第 5.1 节的原则,输入框内容属于页面内部状态(useState),列表数据如果只是本页使用也放页面内;如果未来多个页面要共享待办数据,就要考虑提升或全局化。现在写这个练习时,先把状态放对地方(组件内),等第 5 章学了全局方案再决定要不要升级。这个"先局部、后按需升级"的路径,比一上来就全局化更符合实际开发节奏。

3.4.1 给练习加一个"完成"交互

把待办练习升级一步:点击列表项标记完成(文字加删除线)。这个改动虽小,却触及本章与第 5 章的交汇——列表项的渲染依赖状态(completed 字段),点击要更新状态并触发重新渲染。做法是给 items 数据加 completed 布尔字段,renderItem 里根据字段决定是否加删除线样式,点击回调里更新对应项的状态。这一步做下来,你就完成了"状态驱动界面"从概念到实践的完整闭环。

核心回顾

  • 同一需求两种解法:输入、按钮、列表在 RN 与 Flutter 各有对应组件。
  • RN 受控输入:value 绑定状态、onChangeText 更新,界面永远等于状态。
  • Flutter 控制器输入:TextEditingController 持有内容,需要时取值。
  • FlatList 三要素:data、keyExtractor、renderItem,缺一不可。
  • ListView.builder:itemCount 加 itemBuilder,概念与 FlatList 相同。
  • key 必须稳定:用唯一 id,别用数组下标。
  • 控制器释放:Flutter 的 controller 用完要在 dispose 里清理。
  • 虚拟渲染:FlatList 与 ListView.builder 只渲染可视区域,数据量大也流畅。
  • 空状态与加载:真实列表要处理数据为空与加载中的界面。
  • 状态统一驱动:越复杂的功能,越要让状态成为界面的唯一来源。

组件的组合能力已经验证,下一节处理最后一个挑战——不同屏幕尺寸下,界面怎么保持不崩、不失衡。这也是组件体系从"能用"到"好用"的关键一跃。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U