Flutter跨dart文件调用计时子组件按钮点击无响应问题
问题核心原因
- 手动实例化
State类是Flutter开发的典型错误用法:State对象由框架在组件挂载时自动创建、绑定渲染上下文、管理生命周期,手动调用TimeState().buiildTime()、TimeState().buildButtons()生成的两个TimeState实例完全脱离组件树管控:- 实例的
initState不会自动执行,初始状态异常 - 实例调用
setState不会触发任何界面刷新 - 两个实例各自持有独立的计时、定时器变量,按钮操作修改的状态和时间显示读取的状态完全不互通,点击按钮自然没有任何响应
- 实例的
time.dart中Time组件的build方法返回了独立的Scaffold,作为子组件嵌入页面时会出现嵌套Scaffold的渲染异常。- 时间显示方法存在拼写错误:
buiildTime少写了字母l,容易引发调用异常。
修复方案
1. 重构time.dart
移除多余的嵌套Scaffold,通过InheritedWidget实现单例计时状态共享,开放独立的时间显示、控制按钮渲染方法,保证两处组件操作同一份计时状态:
import 'dart:async'; import 'package:flutter/material.dart'; import 'button.dart'; class Time extends StatefulWidget { const Time({Key? key}) : super(key: key); // 提供全局获取共享计时状态的方法 static TimeState of(BuildContext context) { final _TimeInherited? result = context.dependOnInheritedWidgetOfExactType<_TimeInherited>(); assert(result != null, 'No TimeState found in context'); return result!.state; } @override State<Time> createState() => TimeState(); } class TimeState extends State<Time> { Duration duration = Duration(); Timer? timer; late String time; final myController = TextEditingController(); @override void initState() { super.initState(); reset(); } void reset() { setState(() => duration = Duration()); } void addTime() { const addSeconds = 1; setState(() { final seconds = duration.inSeconds + addSeconds; duration = Duration(seconds: seconds); time = duration.toString(); }); } void startTimer({bool resets = true}) { if (resets) reset(); timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); } void stopTimer({bool resets = true}) { if (resets) reset(); setState(() { timer?.cancel(); }); } void firstHalfEnd() { setState(() => duration = const Duration(minutes: 45, seconds: 0)); stopTimer(resets: false); } void secondHalfEnd() { setState(() => duration = const Duration(minutes: 90, seconds: 0)); stopTimer(resets: false); } void addMinute() { setState(() { duration = duration + const Duration(minutes: 1); }); } void subtractMinute() { setState(() { duration = duration - const Duration(minutes: 1); }); } @override Widget build(BuildContext context) { // 通过InheritedWidget向子树共享当前State实例 return _TimeInherited( state: this, child: const SizedBox.shrink(), ); } // 独立的时间显示组件 Widget buildTimeDisplay() { String twoDigits(int n) => n.toString().padLeft(2, '0'); final minutes = twoDigits(duration.inMinutes.remainder(100)); final seconds = twoDigits(duration.inSeconds.remainder(60)); return Text( '$minutes:$seconds', style: const TextStyle(fontSize: 20, color: Colors.white), ); } // 独立的计时控制按钮组件 Widget buildControlButtons() { bool timeGoing = timer?.isActive ?? false; bool timeStops = duration.inSeconds == 0; return timeGoing || !timeStops ? Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ ButtonWidget( text: timeGoing ? 'PAUSE' : 'RESUME', onClicked: () { if (timeGoing) { stopTimer(resets: false); } else { startTimer(resets: false); } }), const SizedBox(width: 12), ButtonWidget(text: "RESET", onClicked: reset), ], ), const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonWidget( text: '1. Half END', color: Colors.white, backgroundColor: Colors.blue, onClicked: firstHalfEnd, ), const SizedBox(width: 10), ButtonWidget( text: '2. Half END', color: Colors.white, backgroundColor: Colors.blue, onClicked: secondHalfEnd, ), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton(onPressed: addMinute, child: const Text('+1 minute')), const SizedBox(width: 10), ElevatedButton(onPressed: subtractMinute, child: const Text('-1 minute')) ], ) ], ) : ButtonWidget( text: "Start Timer!", color: Colors.black, backgroundColor: Colors.white, onClicked: () => startTimer(), ); } } // 用于跨组件共享状态的私有Inherited组件 class _TimeInherited extends InheritedWidget { final TimeState state; const _TimeInherited({required this.state, required Widget child}) : super(child: child); @override bool updateShouldNotify(covariant _TimeInherited oldWidget) => state != oldWidget.state; }
2. 修改screen_result.dart
在组件树顶层挂载Time组件初始化共享计时状态,移除所有手动实例化TimeState的代码,在需要显示时间、放置控制按钮的位置通过Time.of(context)获取同一个共享状态实例渲染对应组件:
import 'package:flutter/material.dart'; import 'time.dart'; class Screen extends StatefulWidget { @override _ScreenState createState() => _ScreenState(); } class _ScreenState extends State<Screen> { int result_home = 0; int result_away = 0; @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( child: Column(children: <Widget>[ // 顶层挂载Time组件,初始化全局共享计时状态 const Time(), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 主队比分区域 Container( color: Colors.green, height: 500, width: 300, alignment: Alignment.topCenter, child: Text( '$result_home', style: const TextStyle( fontSize: 100.0, color: Colors.white, ), ), ), // 中间分隔与时间显示区域 Column( children: [ Container( color: Colors.green, height: 150, width: 450, alignment: Alignment.topCenter, child: const Text( ':', style: TextStyle( fontSize: 100.0, color: Colors.white, ), ), ), Container( color: Colors.green, height: 350, width: 450, alignment: Alignment.topCenter, // 渲染共享状态的时间显示 child: Time.of(context).buildTimeDisplay(), ), ], ), // 客队比分区域 Container( color: Colors.green, height: 500, width: 300, alignment: Alignment.topCenter, child: Text( '$result_away', style: const TextStyle( fontSize: 100.0, color: Colors.white, ), ), ), ], ), Row( children: [ // 主队比分控制按钮区域 Flexible( fit: FlexFit.tight, flex: 1, child: Container( color: Colors.blue, height: 200, child: Column( children: [ SizedBox( width: 150, height: 100, child: ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.yellow[700], ), onPressed: () { setState(() { result_home++; }); }, child: const Text( "GOAL HOME", style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox( width: 150, height: 100, child: ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.yellow[700], ), onPressed: () { if (result_home > 0) { setState(() { result_home--; }); } }, child: const Text( "NO GOAL", style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], )), ), // 计时控制按钮区域 Flexible( fit: FlexFit.tight, flex: 1, child: Column( children: [ Container( width: 150, height: 100, // 渲染共享状态的计时控制按钮 child: Time.of(context).buildControlButtons() ), ], ), ), // 客队比分控制按钮区域 Flexible( fit: FlexFit.tight, flex: 1, child: Container( color: Colors.blue, height: 200, child: Column( children: [ SizedBox( width: 150, height: 100, child: ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.yellow[700], ), onPressed: () { setState(() { result_away++; }); }, child: const Text( "GOAL AWAY", style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox( width: 150, height: 100, child: ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.yellow[700], ), onPressed: () { if (result_away > 0) { setState(() { result_away--; }); } }, child: const Text( "NO GOAL", style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], )), ), ]), ]))); } }
3. button.dart 无需修改,原有组件逻辑可正常运行。
开发注意事项
- 所有
StatefulWidget对应的State类禁止手动实例化调用,必须通过组件挂载后由BuildContext获取状态实例,否则会出现生命周期失效、状态不共享、界面不刷新的问题。 - 普通子组件不要嵌套独立的
Scaffold,单页面仅需顶层保留一个Scaffold即可,避免出现渲染、路由、弹窗异常。
内容的提问来源于stack exchange,提问作者Robert Tomic
相关产品推荐
相关产品推荐

