Flutter 零基础入门系列走到第二十四篇,终于轮到 Button 按钮体系了。很多初学者心里想的是:按钮不就是点击一下、触发个回调吗?有什么好讲的。但等你真正用 Flutter 写界面的时候就会意识到,按钮远没有想象中那么简单——光是一个 ElevatedButton 就有十来个样式参数,再加上 TextButton、OutlinedButton、IconButton、FloatingActionButton、DropdownButton 这一大家子,每个按钮的定位、默认行为、禁用态样式都不一样。更别提 Flutter 里没有 HTML 那种"点击就有效"的天然交互模型,一切都要自己控制状态、控制回调、控制样式。这篇文章就把我踩过的坑、摸清的门道一次性讲明白,适合刚看完 Flutter 基础语法、想系统掌握控件用法的同学,也适合写过几轮按钮但总觉得"样式调不动"的开发者。
1. 先搞懂设计思路:Flutter 为什么把按钮拆成这么多种
1.1 按钮的分类本质是"视觉层级"的分类
用过其他前端框架的朋友应该都知道,Web 端最常用的按钮就是<button>一个标签,加个 class 换皮肤;小程序里也就是button组件配几个属性。Flutter 不一样,它把按钮拆成了六七种类型,第一次接触的人会觉得很啰嗦,但理解了 Material Design 的设计语言后就明白了:这不是功能上的拆分,而是视觉层级上的拆分。
Material Design 把界面元素分成三个层级:高优先级操作、中优先级操作、低优先级操作。对应到按钮上就是:
- ElevatedButton(填充按钮):有背景色、有阴影,视觉重量最重,适合页面里最核心的那个操作,比如"登录""提交订单"。
- OutlinedButton(描边按钮):没有填充但有边框,视觉重量适中,适合次级操作,比如"取消""暂存"。
- TextButton(文字按钮):纯文字没有边框没有背景,视觉重量最轻,适合低频操作或者需要弱化的入口,比如"忘记密码""查看更多"。
这个设计逻辑跟 iOS 的"实心按钮/幽灵按钮/文字按钮"是相通的。我在实际项目里最常遇到的错误是:新手把所有的点击入口全部做成 ElevatedButton,结果整个页面全是"实心大按钮",视觉上没有主次,用户反而不知道该点哪里。正确做法是利用三种按钮拉开层级——一个页面里核心操作用实心,辅助操作用描边,文字入口用纯文字,界面立刻干净很多。
1.2 六个核心成员的定位表
除了上面三个"三兄弟",Flutter 里还有几个功能型的按钮,它们不是靠视觉层级区分,而是靠承载内容区分:
| 按钮类型 | 默认外观 | 典型用途 | 关键参数 |
|---|---|---|---|
| ElevatedButton | 带填充色和阴影 | 页面主操作 | onPressed, style, child |
| TextButton | 纯文字 | 弱操作、文字入口 | onPressed, style, child |
| OutlinedButton | 带边框无填充 | 次级操作 | onPressed, style, child |
| IconButton | 只有一个图标 | 工具栏、列表行内图标操作 | onPressed, icon, tooltip |
| FloatingActionButton | 圆形悬浮按钮 | 页面全局快捷操作 | onPressed, child, backgroundColor |
| DropdownButton | 下拉选择按钮 | 选项筛选、切换 | value, items, onChanged |
还有个经常被忽略的PopupMenuButton,点击后弹出菜单,适合"更多操作"这类场景,不需要自己管理弹窗状态。我后面会展开讲几个最常用的。
1.3 和其他前端框架相比,Flutter 按钮的优缺点
这段时间"Flutter 和别的前端框架的优缺点"一直是社区热门话题,单从按钮这一个组件上就能看出不少端倪。Flutter 按钮最大的优点是统一性:不管 Android、iOS、Windows 还是 Web 上跑,同一个 ElevatedButton 在每一端的渲染结果几乎完全一致,不会像原生开发那样在 iOS 和 Android 上各写一套。而且样式定制能力强,通过 style 参数几乎能控制每个像素。
缺点也很明显:学习曲线比 CSS 按钮陡。CSS 里改按钮就是写 background、border、padding,Flutter 里这套东西被包进了 ButtonStyle、MaterialStateProperty 这些概念里,新手容易绕晕。另外 Flutter 的按钮是Material 设计体系的产物,如果你做的是 iOS 风格强烈的应用,用默认按钮会有明显的"Android 味",需要额外花时间去定制。搞明白这个底层差异,后面学什么都快。
2. 核心参数拆解:onPressed、child 和 style 三件套
2.1 onPressed 的两种状态:可点与禁用
每个按钮最重要的参数就是onPressed,它是一个回调函数,用户点击时触发。这里有一个新手很容易忽略的知识点:onPressed 为 null 时,按钮自动进入禁用状态。
// 可点击状态 ElevatedButton( onPressed: () { print('按钮被点击了'); }, child: const Text('登录'), ) // 禁用状态:onPressed 传 null ElevatedButton( onPressed: null, child: const Text('登录'), )从 Flutter 2.x 到现在的 3.x,禁用态的默认样式一直是:文字变灰、背景变淡、阴影消失。这个设计比 Web 端要严谨得多——HTML 里如果只是忘了写 onclick,按钮照样亮堂堂的,用户点了没反应,体验很糟糕。Flutter 直接通过视觉告诉你"这个按钮现在不能用"。
实际开发中,我通常会写一个"表单是否有效"的判断,直接把onPressed绑定到一个条件表达式上:
ElevatedButton( onPressed: _formIsValid ? _submit : null, child: const Text('提交'), )这样只要表单没填完,提交按钮就是置灰状态,用户一眼就能看出来。比起"等用户点了一下再弹提示",这种前置控制要友好得多。
2.2 styleFrom:最快的样式定制入口
styleFrom是 Flutter 提供的一个静态方法,专门用于快速生成按钮样式,不需要手写一堆嵌套的MaterialStateProperty。对于大多数业务场景,用styleFrom已经绰绰有余。
ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, elevation: 4, padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), textStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), child: const Text('自定义按钮'), )这里每个参数的"为什么"值得说一下:
backgroundColor是背景色,注意不是color。很多人从原生 UI 的习惯过来,会下意识找 color 参数,但在 Flutter 按钮里背景统一用backgroundColor,文字和图标颜色用foregroundColor。elevation是阴影高度。Material 设计里阴影高度表示元素"浮起来"的程度,按钮点击时阴影会自动变化,这是默认交互的一部分。shape控制形状,最常用的是RoundedRectangleBorder做圆角。需要注意:一旦设置了 shape,默认的圆角会被覆盖,如果不写就沿用主题里的默认圆角。textStyle控制文字样式,但要注意它会被foregroundColor影响。如果foregroundColor设成白色,文字就是白色,textStyle里再指定颜色会以 textStyle 为准。
用styleFrom还有一个好处:它支持全局主题统一配置。在ThemeData里给所有 ElevatedButton 定一套默认样式,页面里就能少写很多重复代码。
MaterialApp( theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, minimumSize: const Size(88, 44), ), ), ), )这算是 Flutter 项目里的"基础设施"工作,提前把按钮主题配好,后面写页面能省一半时间。
2.3 ButtonStyle 与 WidgetStateProperty:深度定制绕不开的概念
当styleFrom满足不了需求时,就需要直接构建ButtonStyle对象。ButtonStyle里每个属性几乎都是WidgetStateProperty类型,它的作用是根据按钮状态返回不同的值。
ElevatedButton( onPressed: () {}, style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith( (states) { if (states.contains(WidgetState.pressed)) { return Colors.blue; // 按下时颜色 } if (states.contains(WidgetState.disabled)) { return Colors.grey.shade300; // 禁用时颜色 } return Colors.blueAccent; // 默认颜色 }, ), ), child: const Text('动态变化按钮'), )这里要注意,Flutter 3.19 之前的版本里这个类叫MaterialStateProperty,新版本改名为WidgetStateProperty。网上很多教程还在用旧名字,如果你用的是较新版本,代码里写MaterialStateProperty会因为 API 变更而标黄甚至报错。这算是最近社区里问得比较多的问题之一。
WidgetStateProperty.resolveWith接收一个Set<WidgetState>参数,里面包含当前按钮处于哪些状态。常见的状态枚举有:pressed(按下)、hovered(悬停,桌面端生效)、focused(聚焦)、disabled(禁用)。有了这个机制,你就能实现各种复杂的交互效果,比如按下去变深色、松手恢复、禁用再变灰。
还有一个高频需求是给按钮加渐变背景。ButtonStyle不支持直接传渐变,但可以通过shape或者自定义Ink来实现,更常见的做法是用Container包一层然后套Material,不过那样会丢失按钮自带的 ripple 水波纹效果。我在项目里通常是用Ink+InkWell自己组合,或者干脆用第三方库,但不建议新手一上来就搞渐变,先把基础样式玩明白更重要。
3. 交互状态实战:禁用、防重复点击、加载中
3.1 按钮禁用态的细节处理
禁用态不是"变灰"这么简单。很多产品需求里,禁用按钮不能只是颜色变化,还可能要同时禁用点击、隐藏阴影、改变鼠标光标。Flutter 默认处理了点击和阴影,但有些细节需要自己补:
- Tooltip 提示:禁用按钮用户不知道为什么不能点,可以在按钮外面包一个
Tooltip组件,悬停显示"请先填写邮箱"。 - 语义化:对于无障碍支持,Flutter 在禁用状态下会自动把按钮标记为不可操作,这个不用额外写。
- 禁用态样式覆盖:如果觉得默认灰色太丑,可以单独定一套禁用样式。用
styleFrom时,禁用背景色需要用WidgetStatePropertyAll或者其他方式单独指定:
ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, disabledBackgroundColor: Colors.grey.shade200, disabledForegroundColor: Colors.grey.shade500, )disabledBackgroundColor这个参数是styleFrom专门提供的快捷参数,比手写ButtonStyle方便得多,很多教程都没提,算是一个小宝藏。
3.2 防重复点击:从最简单的到适合生产的方案
"限制一段时间内对 button 只能点按一次"是我看到很多团队都在搜的问题,不止 Flutter,Qt、Web、小程序里都有。原因很现实:用户手一抖,或者接口响应慢,双击提交就会发出两个请求,导致订单重复、表单重复提交。
Flutter 里最朴素的防抖写法是一个布尔标志位:
bool _isSubmitting = false; Future<void> _handleSubmit() async { if (_isSubmitting) return; _isSubmitting = true; try { await api.submit(...); } finally { _isSubmitting = false; } }因为 Flutter 是单线程模型,Dart 的async/await在执行到await时会挂起,所以_isSubmitting = true之后,即使finally还没执行,其他点击事件进来也会被if拦截住。这个方案够用,但有个问题:每次都要手动写状态,按钮 UI 上也没有任何反馈。
更完整的方案是配合StatefulWidget的状态管理:点击时把按钮变成一个带 spinner 的加载状态,同时拦截后续点击。这个在下面的"加载状态"小节一起讲。如果你项目里已经用了 Provider(现在"flutter provider 怎么用"是搜索大热词),还可以把_isSubmitting放进 ViewModel,按钮只负责消费状态,这样多个按钮共享同一个"提交中"状态也没有问题。
3.3 加载状态按钮的实现
生产环境里最常见的按钮形态是"点击 -> 转圈加载 -> 完成"。实现起来其实就是动态替换按钮的 child:
class SubmitButton extends StatelessWidget { final bool loading; final VoidCallback? onPressed; const SubmitButton({ super.key, required this.loading, required this.onPressed, }); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: loading ? null : onPressed, style: ElevatedButton.styleFrom( minimumSize: const Size(120, 44), ), child: loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('提交'), ); } }注意三个细节,都是我实际踩过的坑:
CircularProgressIndicator必须放进SizedBox限定尺寸,否则它默认会撑满按钮可用空间,转圈会变得巨大。- 加载时把
onPressed设为 null,按钮自动禁用,这样既防了重复点击,UI 也明确"正在处理中"。 - 如果按钮文字和 spinner 颜色都是白色,
CircularProgressIndicator的颜色要单独指定,否则可能看不见。
另外,如果按钮在加载时尺寸有变化(比如文字变成 spinner 后变窄了),建议在普通状态也设置minimumSize或者固定宽度,避免界面抖动。这个问题很常见,按钮加载前后宽度不一致,用户看着会觉得很突兀。
4. 特殊按钮逐个看:图标、悬浮、下拉
4.1 IconButton:图标按钮的定位与常见问题
IconButton 是列表页里出现频率最高的按钮,删除、编辑、分享、收藏全都是它。它没有 child,取而代之的是 icon 参数和 tooltip 参数:
IconButton( icon: const Icon(Icons.delete), tooltip: '删除', onPressed: () {}, )tooltip参数很多人会忽略,但它很实用:在移动端长按会弹出提示文字,在桌面端悬停会显示 tooltip,对用户理解图标含义帮助很大。尤其是纯图标没有文字说明的情况下,这是一个低成本提升可用性的手段。
IconButton 的坑主要在命中区域。Flutter 默认 IconButton 的最小点击区域是 48x48 逻辑像素,这是 Material 设计规范里的建议。如果你把 IconButton 放在一个紧凑的列表行里,记得调整constraints或padding,否则会出现"图标看着小,点起来范围很大"或者反过来"图标贴着文字,误触频繁"的问题。
IconButton( icon: const Icon(Icons.more_vert), onPressed: () {}, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), padding: EdgeInsets.zero, )减小constraints后要同时处理 padding,否则图标周围还会有系统默认的 8 像素间隙,两套空间叠加起来视觉上并不居中。
4.2 FloatingActionButton:悬浮按钮的定位与复用
FloatingActionButton 是 Scaffold 支持的全局快捷按钮,一般放在右下角,承载"新建""添加"这类高频操作。它可以直接作为 Scaffold 的floatingActionButton参数传入:
Scaffold( floatingActionButton: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.add), ), )有几个变体值得了解。FloatingActionButton.extended是带文字的扩展形态,适合底部导航栏场景:
FloatingActionButton.extended( onPressed: () {}, icon: const Icon(Icons.add), label: const Text('新建任务'), )还要注意一个细节:FAB 默认会跟底部内容重叠,需要给列表底部加足够的 padding。常见做法是在 ListView 的padding里加一个EdgeInsets.only(bottom: 80),或者用bottomNavigationBar时把 FAB 的location调整为FloatingActionButtonLocation.endDocked。
4.3 DropdownButton:下拉选择按钮的状态管理
DropdownButton 是表单场景里常用的"按钮型控件",点击后弹出下拉列表。它跟普通按钮最大的不同是:它必须自己维护一个当前选中值,否则选中后 UI 不会变化。
String _selectedCity = '北京'; DropdownButton<String>( value: _selectedCity, items: ['北京', '上海', '广州'] .map((city) => DropdownMenuItem<String>( value: city, child: Text(city), )) .toList(), onChanged: (value) { setState(() { _selectedCity = value ?? _selectedCity; }); }, )这里"为什么必须存 value"的原理也适用于 Flutter 的状态管理整体认知:Flutter 是声明式 UI,控件本身不记忆状态,状态完全由外部数据驱动。这也是为什么 Flutter 社区如此强调 Provider、Riverpod 等状态管理库——组件通信本质上就是在不同的 widget 之间共享和同步这些状态。你要是理解了 DropdownButton 的这个"受控组件"模式,再看 TextField、Checkbox、Switch,全都一脉相承,学习成本会低很多。
还有一个容易踩的坑:items里的DropdownMenuItem.value必须跟value的类型完全一致,而且当前value必须在 items 中存在,否则会报错There should be exactly one item with [DropdownButton]'s value。比如把_selectedCity初始值写错、或者 items 被过滤后当前值不在列表里,就会遇到这个异常。
5. 实操记录:从零搭一个按钮测试台页面
5.1 项目初始化与代码结构
纸上谈兵不如动手。我建议每个初学者都建一个"按钮实验室"页面,把所有按钮形态集中放上去,边调样式边看效果。这里我直接分享我自己的做法。
先创建一个新项目:
flutter create button_lab然后打开lib/main.dart,只保留一个ButtonLabPage的 StatefulWidget。我习惯用 ListView 把所有按钮竖排展示,每类按钮一个小节,方便滚动查看。代码结构大概是:
ListView( children: [ _sectionTitle('基础按钮'), ElevatedButton(...), TextButton(...), OutlinedButton(...), _sectionTitle('带图标按钮'), ElevatedButton.icon(...), OutlinedButton.icon(...), _sectionTitle('状态演示'), _LoadingButton(...), _DisabledButton(...), _sectionTitle('特殊按钮'), DropdownButton(...), PopupMenuButton(...), ], )这里用 ListView 而不是 Column 的原因很现实:按钮种类多,竖屏手机上很容易超出屏幕,ListView 自带滚动,不需要额外处理溢出问题。
5.2 核心代码与实现注解
下面是我实际在项目里跑过的测试台核心代码片段,每个按钮都标注了使用场景注解:
// 1. 带图标的主按钮:登录/注册场景最常用 ElevatedButton.icon( onPressed: () {}, icon: const Icon(Icons.login), label: const Text('登录'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ) // 2. 描边图标按钮:购物车"去结算"旁边的"继续购物" OutlinedButton.icon( onPressed: () {}, icon: const Icon(Icons.shopping_cart), label: const Text('继续购物'), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.blueAccent, width: 1.5), ), )注意ElevatedButton.icon和ElevatedButton的区别:前者是构造函数级的内置便捷方法,icon 会作为前置图标排列在文字左侧,并且自动处理图标和文字之间的间距。如果你用Row手动排图标和文字,还要自己算间距和对齐,没必要。
5.3 运行验证与调试心得
写完测试台,在 Android 模拟器上跑一下。这里我要多说一句:最近很多人用最新的 Flutter 版本创建项目后跑不起来("flutter新建项目后跑不起来"是热门搜索词),多半是 Gradle 配置或者本机缓存的问题。我遇到过一次 Android 端构建失败,错误信息指向flutter的 Gradle 插件版本和项目不匹配,最后是升级了 Android Gradle Plugin 版本解决的。如果你是纯 UI 学习阶段,可以先跑 Web 端或者桌面端来绕过 Android 构建的坑,flutter run -d chrome就能看到实时效果,调试按钮样式效率反而更高。
运行测试台后,重点检查几个细节:
- 点击按钮时有没有水波纹效果(默认有,如果用其他方式自定义了背景可能丢失)。
- 禁用按钮颜色是否跟可用按钮有足够对比度。
- 加载态按钮切换时,宽度是不是稳定。
- 在桌面端把鼠标悬停在按钮上,是否和移动端按下的状态区分明显。
这些检查点看起来琐碎,但就是这些细节决定了按钮体验的完成度。
6. 常见问题排查:按钮开发里那些"莫名其妙"的 bug
6.1 按钮点击没反应,onPressed 就是不走
最经典的问题。排查顺序建议是:
- 看
onPressed是不是 null。如果是 null,按钮处于禁用态,怎么点都没反应。 - 看按钮外面有没有遮挡。比如 Stack 里后面的组件盖住了按钮,或者父容器上有
IgnorePointer、AbsorbPointer。 - 看按钮是否在
ListView/SingleChildScrollView之外的区域被手势竞争。桌面端尤其容易出现GestureDetector包住大量区域的情况。 - 看控制台有没有异常输出。Flutter 里如果 build 过程中抛了异常,界面可能卡在上一帧,按钮看着正常但其实事件系统已经崩溃。控制台中的红色错误日志要优先处理。
一个容易被忽略的细节:如果按钮是包在InkWell、GestureDetector里实现的"伪按钮",水波纹效果和点击回调会跟 Flutter 原生的按钮事件重叠,导致点击一次回调触发两次。能用原生按钮尽量用原生按钮,不要手痒自己去组合。
6.2 按钮文字溢出或图标没有垂直居中
文字溢出通常发生在两种场景:按钮宽度不够,或者文字过长。解决思路:
ElevatedButton( onPressed: () {}, child: const Text( '这是一个比较长的按钮文案', maxLines: 1, overflow: TextOverflow.ellipsis, ), style: ElevatedButton.styleFrom( minimumSize: const Size(160, 44), ), )maxLines: 1配合overflow: TextOverflow.ellipsis保证文字永远单行,超出的部分显示省略号。minimumSize给一个最小宽度,避免按钮在极端窄屏下被压到很小。
图标和文字垂直不居中的问题,多半是因为你手动用Row拼的Icon和Text,而没有用ElevatedButton.icon。Icon和Text默认的textBaseline不同,直接放进 Row 里视觉上会看起来底部没对齐。改用.icon系列构造函数,或者给Row加crossAxisAlignment: CrossAxisAlignment.center能缓解。
6.3 设置了 style 但不生效?主题优先级的问题
这是 3.x 版本之后出现频率极高的问题:在ThemeData里配了全局按钮主题,但在某个页面里单独设置style覆盖,结果发现某些属性还是沿用全局的。
原因在于styleFrom生成的ButtonStyle会把没有显式指定的属性留空(null),Flutter 在合并样式时会用"空值保留、非空值覆盖"的规则。也就是说,如果你在页面级只设置了backgroundColor,但全局主题里配了minimumSize,那么minimumSize依然会生效。这不是 bug,是设计如此。
如果需要彻底覆盖某个属性,必须显式传值。比如:
style: ElevatedButton.styleFrom( backgroundColor: Colors.red, minimumSize: const Size(200, 48), // 想完全覆盖,必须自己显式赋值 )遇到"样式不生效"时,优先检查是不是存在全局主题和页面级样式的"空值继承"逻辑。
6.4 按钮状态没有随数据变化刷新
很多新手问为什么_isSubmitting改了之后按钮没有变成 loading 态,或者 DropdownButton 选完之后界面没反应。根本原因都一样:数据变了,但没有触发 rebuild。
在 StatefulWidget 里必须调用setState;在 Provider 里必须确保数据改变后能通知监听者。Flutter 不会主动"监听"你的变量值变化,它需要显式的刷新信号。这也是 Flutter 学习和面试里反复被问的"组件通信"问题——父组件怎么把状态传下来、子组件怎么把事件传上去。按钮回调(onPressed)本质上就是子组件向父组件"上报事件"的通路,而父组件通过参数把最新状态传回按钮。理解了这条单向数据流,状态刷新问题就不会再困惑。
6.5 按钮渲染缓慢或出现奇怪的渲染问题
按钮数量特别多的列表页,可以考虑把按钮的 child 改成const构造,减少重建开销。Flutter 的 const widget 在编译期就能复用,运行时不需要重新构建,性能会好一些。
还有一个小众但是值得知道的问题:Flutter 新版默认启用 Impeller 渲染引擎后,个别低端 Android 设备上按钮的水波纹或阴影可能出现轻微异常。如果你在真机上遇到按钮外观跟模拟器不一致,可以尝试在AndroidManifest.xml或命令行关闭 Impeller 对比验证。等 Impeller 稳定性进一步提升后,这个手动开关会逐渐退出历史舞台,但排查思路仍然通用——渲染层的差异可以用"换回 Skia 渲染"来定位是不是引擎问题。
写在最后的一点经验
按钮是整个 Flutter 控件体系里最"小而全"的组件,它涉及样式、状态、回调、主题、语义化、性能优化,几乎覆盖了 Flutter 开发的所有核心概念。我个人带新人时,特别喜欢用"做一套按钮测试台"来检验他们对 Flutter 的掌握程度——能独立完成按钮禁用态切换、防重复点击、加载态、下拉按钮联动这几个需求,基础就算扎实了。
如果你正在准备 Flutter 面试,Button 的考察点通常集中在:onPressed为 null 时的行为、styleFrom和ButtonStyle的区别、WidgetStateProperty的用法、以及状态刷新机制。把这篇文章里的代码自己敲一遍,这些面试点基本都能覆盖。
最后分享一个小技巧:养成看源码的习惯。按住 Ctrl 键点击ElevatedButton,你能看到它内部其实还是用ButtonStyleButton这个基类去渲染的。看懂一次源码,后面学 TextField、Switch、Checkbox 这些控件,会发现它们全都是同一套"样式 + 状态 + 回调"的套路。这才是零基础进阶到有基础的正确姿势。