Flutter Home页搜索城市后数值未更新 setState异常问题
问题定位
你的页面数据不更新是多个逻辑问题共同导致的,核心问题如下:
getData方法上多余了错误的@override注解,且posts变量的赋值操作写在了setState外部,异步场景下容易出现状态和UI同步异常- 发起新搜索请求前没有重置加载状态,也没有做输入合法性校验,空输入、错误城市名导致请求失败时,不会触发UI更新,旧数据会一直残留
- 缺少异常捕获和请求资源释放逻辑,接口报错时会静默失败,没有任何用户提示
- 气压值没有做边界处理,数值超出合法范围时会打断环形进度条的渲染流程
- 缺少初始化生命周期调用,首次进入页面不会自动加载默认城市数据
修复方案
- 移除
getData上错误的@override注解,重写initState生命周期,首次进入页面加载默认城市天气 - 所有和UI相关的状态赋值操作全部放到
setState回调内部执行 - 发起请求前先将
isLoaded置为false,展示加载状态,请求结束(无论成功失败)再恢复 - 增加输入非空校验,请求过程捕获异常,失败时给用户提示
- 对气压值做边界裁剪,保证环形进度条的
percent值始终在0~1区间 - 请求结束后及时关闭http客户端,释放资源
修复后完整代码
HomePage修复代码
import 'package:flutter/material.dart'; import 'package:glass_kit/glass_kit.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:percent_indicator/percent_indicator.dart'; import '../model/model.dart'; import '../services/remote_services.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { Post? posts; bool isLoaded = true; double? pressure; final TextEditingController city = TextEditingController(); // 首次加载默认城市,可自行修改 final String defaultCity = "Beijing"; @override void initState() { super.initState(); // 首次进入页面加载默认城市天气 getData(defaultCity); } Future<void> getData(String searchCity) async { // 空输入直接拦截 if(searchCity.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("请输入城市名称")) ); return; } // 请求开始前切换加载状态 setState(() { isLoaded = false; }); try { Post? newPosts = await RemoteService().getPosts(searchCity); if(newPosts != null) { // 所有UI相关状态更新全部放在setState内部 setState(() { posts = newPosts; pressure = posts?.main.pressure; isLoaded = true; }); } else { // 接口返回非200状态(城市不存在、参数错误等) setState(() { isLoaded = true; }); if(mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("未查询到该城市天气,请检查城市名称")) ); } } } catch (e) { // 捕获网络异常、解析异常 setState(() { isLoaded = true; }); if(mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("网络异常,请稍后重试")) ); } } } @override Widget build(BuildContext context) { return Scaffold( body: Container( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage("assets/images/Clouds.jpg"), fit: BoxFit.cover, ), ), child: SafeArea( child: SingleChildScrollView( child: Visibility( visible: isLoaded, // 加载状态展示loading组件 replacement: const Center( child: Padding( padding: EdgeInsets.all(100), child: CircularProgressIndicator(color: Colors.white,), ), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Row( children: [ GlassContainer.clearGlass( borderWidth: 0, height: 50, width: MediaQuery.of(context).size.width-70, borderRadius: BorderRadius.circular(10), child: TextField( controller: city, textAlign: TextAlign.center, style: const TextStyle( color: Colors.white ), decoration: const InputDecoration( hintText: "Enter City", hintStyle: TextStyle(color: Colors.white70), border: InputBorder.none, ), // 支持键盘回车直接触发搜索 onSubmitted: (value) { getData(city.text.trim()); }, ), ), const SizedBox(width: 10,), GlassContainer.clearGlass( height: 50, width: 50, borderWidth: 0, borderRadius: BorderRadius.circular(10), child: Center( child: IconButton( onPressed: (){ // 自动去除输入首尾空格,避免无效请求 getData(city.text.trim()); }, icon: const Icon(Icons.search,color: Colors.white,), ), ), ) ], ), const SizedBox(height: 5,), Row( children: [ Column( children: [ GlassContainer.clearGlass( height: MediaQuery.of(context).size.height/8.099, width: MediaQuery.of(context).size.width/3.990, borderWidth: 0, borderRadius: BorderRadius.circular(10), blur: 10, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon(FontAwesomeIcons.water, color: Colors.white,), Text( posts==null?" ":"${posts?.main.humidity}%", style: const TextStyle( color: Colors.white, fontSize: 24 ), ) ], ), ), const SizedBox(height: 5,), GlassContainer.clearGlass( height: MediaQuery.of(context).size.height/8.099, width: MediaQuery.of(context).size.width/3.990, borderWidth: 0, borderRadius: BorderRadius.circular(10), blur: 10, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon(FontAwesomeIcons.wind, color: Colors.white,), Text( posts==null?" ":"${posts?.wind.speed}", style: const TextStyle( color: Colors.white, fontSize: 24 ), ), const Text( "Km/h", style: TextStyle( fontSize: 12, color: Colors.white ), ) ], ), ) ], ), const Expanded(child: SizedBox()), GlassContainer.clearGlass( width: MediaQuery.of(context).size.width/1.51102941, height: MediaQuery.of(context).size.height/3.77731092-10, borderRadius: BorderRadius.circular(10), borderWidth: 0, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( posts==null?" ":"${posts?.main.temp} C", style: const TextStyle( fontSize: 75, color: Colors.white, ), ), const SizedBox(height: 10,), Text( posts==null?"":"Feels like ${posts?.main.feelsLike} C", style: const TextStyle( fontSize: 18, color: Colors.white ), ) ], ), ), ), const Expanded(child: SizedBox()), ], ), const SizedBox(height: 7,), Row( children: [ GlassContainer.clearGlass( width: MediaQuery.of(context).size.width/1.51102941, height: MediaQuery.of(context).size.height/3.77731092-10+12, borderRadius: BorderRadius.circular(10), borderWidth: 0, child: CircularPercentIndicator( radius: 90.0, lineWidth: 13.0, animation: true, // 裁剪percent值到0-1合法区间,避免渲染报错 percent: pressure==null?0: (pressure!/1084).clamp(0.0, 1.0), center: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon((pressure!=null)?(pressure!>1013)?FontAwesomeIcons.arrowUp:FontAwesomeIcons.arrowDown:FontAwesomeIcons.arrowDown, color: Colors.white,), Text( "${pressure ?? ''} mB", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 30, color: Colors.white), ), ], ), circularStrokeCap: CircularStrokeCap.round, progressColor: Colors.white, ), ), const Expanded(child: SizedBox()), Column( children: [ GlassContainer.clearGlass( height: MediaQuery.of(context).size.height/8.099, width: MediaQuery.of(context).size.width/3.990, borderWidth: 0, borderRadius: BorderRadius.circular(10), blur: 10, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon(FontAwesomeIcons.temperatureArrowUp, color: Colors.white,), Text( posts==null? " " : "${posts?.main.tempMax} C", style: const TextStyle( color: Colors.white, fontSize: 24 ), ) ], ), ), const SizedBox(height: 18,), GlassContainer.clearGlass( height: MediaQuery.of(context).size.height/8.099, width: MediaQuery.of(context).size.width/3.990, borderWidth: 0, borderRadius: BorderRadius.circular(10), blur: 10, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon(FontAwesomeIcons.temperatureArrowDown, color: Colors.white,), Text( posts==null? " " : "${posts?.main.tempMin} C", style: const TextStyle( color: Colors.white, fontSize: 24 ), ) ], ), ), ], ), const Expanded(child: SizedBox()) ], ), ], ), ), ), ), ), ), ); } }
RemoteService修复代码
import 'package:http/http.dart' as http; import '../model/model.dart'; class RemoteService{ Future <Post?> getPosts(String city) async{ http.Client client = http.Client(); try { var uri = Uri.parse("https://api.openweathermap.org/data/2.5/weather?q=$city&appid=5e749d88f2df02cacc9be6abf8088531&units=metric"); var response = await client.get(uri); if (response.statusCode == 200){ var json = response.body; return postFromJson(json); } return null; } catch (e) { rethrow; } finally { // 无论请求成功失败都关闭客户端,释放连接资源 client.close(); } } }
修复效果
- 首次进入页面自动加载默认城市天气,点击搜索会先展示加载状态,请求成功后所有天气数据正常刷新
- 空输入、错误城市名、网络异常都会弹出对应提示,不会出现点击无反应的情况
- 气压组件做了边界保护,不会因为数值越界打断渲染
- 支持键盘回车直接触发搜索,输入自动去除首尾空格,减少无效请求
内容的提问来源于stack exchange,提问作者Aswin Pratapsingh
相关产品推荐
相关产品推荐

