按钮按下后,为什么修改变量还不够
从修改一个读数开始,引入页面状态和重新构建;再认识页面建立、持续存在和退出这三个时刻。
1 · 读数变化,需要谁知道
把固定文字换成变量以后,如果只在回调中修改变量,框架未必知道需要重新构建。需要一处保存会变化的数据,并在变化时通知框架。这就是本篇引入 State 的原因。
替换上一篇页面类;main 的 home 仍为 BatteryPage
class BatteryPage extends StatefulWidget {
const BatteryPage({super.key});
@override
State<BatteryPage> createState() => _BatteryPageState();
}
class _BatteryPageState extends State<BatteryPage> {
int millivolts = 4000;
@override
Widget build(BuildContext context) => Scaffold(
body: Column(children: [
Text("$millivolts mV"),
ElevatedButton(
onPressed: () => setState(() => millivolts -= 10),
child: const Text("Decrease"),
),
]),
);
}StatefulWidget 创建一个对应的 State 对象。这里 millivolts 保存在 State 中,setState 中的回调立即修改它,并通知框架安排重新构建。下一次 build 读取新数值,生成新的界面描述。
2 · 重新构建,会不会把状态也从头创建
在组件身份保持一致的情况下,框架可以保留原有 State,再用新的组件配置调用它的 build。局部变量若在 build 内每次写成 4000,当然每次又从这个初值开始;需要保留的读数应放在 State 字段中。
build 应根据当前状态描述界面,不执行不可重复的业务操作。频繁重新构建不等于每次都必须把整块屏幕重新画一遍;本篇先理解数据与描述的关系。
3 · 页面还在吗:生命周期从哪里开始
加在 State 类中;具体资源在异步篇创建
@override
void initState() {
super.initState();
// 在这里准备这个 State 生命周期内的一次性监听。
}
@override
void dispose() {
// 在这里停止该页面持有的监听、定时器等资源。
super.dispose();
}initState 在 State 初始化时执行,dispose 在它永久退出时执行。它们不是每次 build 的前后钩子。切换页面、移除组件等操作可能结束这一份 State;隐藏在标签页中是否销毁取决于实际组件组织,不能凭“看不到页面了”推断。
4 · 没有数据,应该显示什么
电量还没收到时,不宜随手显示 0,用户会误以为电池耗尽。可以把字段写为 int?,null 表示尚无当前测量;收到真实读数后再赋值。数据无效或断连时,清除读数或明确标注状态。
状态与显示的对应示意
int? millivolts;
// build 内:
Text(millivolts == null ? "No current measurement" : "$millivolts mV");先只区分“没有测量”和“有测量”;需要显示扫描中、失败等更多情况时再加入对应字段。每个状态都应对应真实事件,不先堆一组尚未用到的枚举。
5 · 怎样证明页面没有依赖真实手表
配套小程序的测试在没有蓝牙设备时点击开始、等待模拟数据,再点击停止,确认旧数值被清除。它验证的是可见行为,不依赖内部变量叫什么名字。后面换成蓝牙输入时,这项行为要求仍然保留。
把初始读数改成“未收到”,增加一个清除按钮。连续修改读数后热重载,观察状态是否保留;完整重启后再比较。说明为什么 build 里不能每次创建新的蓝牙监听。