Flutter中如何仅刷新PageView内当前显示的网络图片?
问题描述
我是Flutter新手,想要开发一个查看多路监控摄像头图片的页面。我使用PageView实现滑动切换摄像头图片,并在PageView外设置了一个刷新按钮,希望点击按钮时仅重新加载当前显示的摄像头图片,其上下的关联图片无需刷新。
目前在刷新按钮的OnTap中调用setState()虽能实现刷新,但会导致PageView出现闪烁。请问是否可以仅刷新目标Image?或者有其他解决方案?
相关代码
main.dart
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'src/app.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); runApp( const MyApp() ); }
src/app.dart
import 'package:flutter/material.dart'; import 'screens/cam.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( //debugShowCheckedModeBanner: false, title: 'Flutter Demo', home: BasePage(), ); } } class BasePage extends StatefulWidget{ @override _BasePage createState() => _BasePage(); } class _BasePage extends State<BasePage> { final _pageController = PageController(); int _currentIndex = 0; void changeIndex(index){ setState(() { _currentIndex = index; _pageController.jumpToPage(index); }); } @override Widget build(BuildContext context) { var TabItem = { //Omit unnecessary codes. 'cam':[3, Icons.video_camera_back, Colors.lightGreen, CamScreen()], }; return Scaffold( body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), children: TabItem.values.map((value) => value[3] as Widget).toList(), ), bottomNavigationBar: SizedBox( height: MediaQuery.of(context).size.height /8, child:BottomNavigationBar( //Omit unnecessary codes. ), ), ); } }
src/screens/cam.dart
import 'package:flutter/material.dart'; import 'dart:math'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class CamScreen extends StatefulWidget { @override _CamScreen createState() => _CamScreen(); } class _CamScreen extends State<CamScreen>{ Future<Map<String, dynamic>> fetchData(int camNumber) async { final response = await http.get(Uri.parse('https://mydomain/get_cam_info?no=$camNumber')); if (response.statusCode == 200) { final jsonData = jsonDecode(response.body); return jsonData; } else { throw Exception('Failed to load data'); } } @override Widget build(BuildContext context) { const camCount = 5; final _pageController = PageController(initialPage: camCount * 5); return Column( children: [ Expanded( flex: 7, child: PageView.builder( controller: _pageController, itemBuilder: (context, index){ return FutureBuilder( future: fetchData(index % camCount + 1), builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting){ return const Center(child: CircularProgressIndicator()); } else if(snapshot.hasError || snapshot.data == null){ return Column(children: [Expanded(child: Image.network('https://mydomain/cam_img?no=${index % camCount +1}&cache=${Random().nextInt(100)}',) )]); } else { final jsonData = snapshot.data!; final camName = jsonData['name']; final upperPath = jsonData['upper']['path']; final upperUrl = jsonData['upper']['url']; final lowerPath = jsonData['lower']['path']; final lowerUrl = jsonData['lower']['url']; return Scaffold( backgroundColor: Colors.black, appBar: AppBar( toolbarHeight: MediaQuery.of(context).size.height /16, title: Text(camName), centerTitle: true, backgroundColor: Colors.orange, ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (upperUrl != null && upperUrl.isNotEmpty && upperPath != null && upperPath.isNotEmpty) Expanded(child: GestureDetector( onTap: () async { await launchUrl(Uri.parse(upperUrl)); }, child: Image.network('https://mydomain/img/$upperPath'), ),), SizedBox( width: MediaQuery.of(context).size.width, child:Image.network('https://mydomain/cam_img?no=${index % camCount +1}&cache=${Random().nextInt(10000)}',) ), if (lowerUrl != null && lowerUrl.isNotEmpty && lowerPath != null && lowerPath.isNotEmpty) Expanded(child:GestureDetector( onTap: () async { await launchUrl(Uri.parse(lowerUrl)); }, child: Image.network('https://mydomain/img/$lowerPath'), ),) ], ), ); } } ); }, ), ), Expanded( flex: 1, child: Row( children: [ Expanded( child: GestureDetector( onTap: (){}, child: Container( decoration: BoxDecoration( color: Colors.indigo.shade300, border: Border.all(width: 2, color: Colors.white), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.location_on, size: 40,), Text('map') ], ) ) ), ), Expanded( child: GestureDetector( onTap: (){ setState(() { }); }, child: Container( decoration: BoxDecoration( color: Colors.indigo.shade300, border: Border.all(width: 2, color: Colors.white), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.refresh, size: 35,), Text('reload') ], ) ) ), ), ], ) ) ], ); } }
解决方案
核心问题分析
当前调用setState()会触发整个CamScreen重绘,包括PageView的所有item,因此出现闪烁。要实现仅刷新当前显示的图片,需要避免全局重绘,改为局部控制单个摄像头页面的刷新。
方案1:将每个摄像头页面封装为独立StatefulWidget
把每个摄像头的内容单独做成一个StatefulWidget,通过GlobalKey控制单个组件的刷新,不影响整个PageView。
步骤1:创建独立的CamItemPage组件
class CamItemPage extends StatefulWidget { final int camNumber; const CamItemPage({super.key, required this.camNumber}); @override State<CamItemPage> createState() => _CamItemPageState(); } class _CamItemPageState extends State<CamItemPage> { late Future<Map<String, dynamic>> _futureData; int _cacheKey = 0; // 用于更新图片缓存参数 @override void initState() { super.initState(); _futureData = fetchData(widget.camNumber); } Future<Map<String, dynamic>> fetchData(int camNumber) async { final response = await http.get(Uri.parse('https://mydomain/get_cam_info?no=$camNumber')); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to load data'); } } // 提供外部调用的刷新方法 void refresh() { setState(() { _cacheKey++; _futureData = fetchData(widget.camNumber); }); } @override Widget build(BuildContext context) { return FutureBuilder( future: _futureData, builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting){ return const Center(child: CircularProgressIndicator()); } else if(snapshot.hasError || snapshot.data == null){ return Column(children: [Expanded(child: Image.network('https://mydomain/cam_img?no=${widget.camNumber}&cache=${_cacheKey}',) )]); } else { final jsonData = snapshot.data!; final camName = jsonData['name']; final upperPath = jsonData['upper']['path']; final upperUrl = jsonData['upper']['url']; final lowerPath = jsonData['lower']['path']; final lowerUrl = jsonData['lower']['url']; return Scaffold( backgroundColor: Colors.black, appBar: AppBar( toolbarHeight: MediaQuery.of(context).size.height /16, title: Text(camName), centerTitle: true, backgroundColor: Colors.orange, ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (upperUrl != null && upperUrl.isNotEmpty && upperPath != null && upperPath.isNotEmpty) Expanded(child: GestureDetector( onTap: () async { await launchUrl(Uri.parse(upperUrl)); }, child: Image.network('https://mydomain/img/$upperPath?cache=${_cacheKey}'), ),), SizedBox( width: MediaQuery.of(context).size.width, child:Image.network('https://mydomain/cam_img?no=${widget.camNumber}&cache=${_cacheKey}',) ), if (lowerUrl != null && lowerUrl.isNotEmpty && lowerPath != null && lowerPath.isNotEmpty) Expanded(child:GestureDetector( onTap: () async { await launchUrl(Uri.parse(lowerUrl)); }, child: Image.network('https://mydomain/img/$lowerPath?cache=${_cacheKey}'), ),) ], ), ); } } ); } }
步骤2:修改CamScreen控制单个页面刷新
class _CamScreen extends State<CamScreen>{ const camCount = 5; final _pageController = PageController(initialPage: camCount * 5); int _currentPage = camCount * 5; // 存储每个摄像头页面的GlobalKey,用于调用刷新方法 final Map<int, GlobalKey<_CamItemPageState>> _itemKeys = {}; @override void initState() { super.initState(); // 初始化每个摄像头的key for(int i=0; i<camCount; i++){ _itemKeys[i] = GlobalKey(); } // 监听页面切换,更新当前页面索引 _pageController.addListener(() { setState(() { _currentPage = _pageController.page?.round() ?? camCount *5; }); }); } // 刷新当前显示的摄像头 void _refreshCurrentCam() { int currentCamIndex = (_currentPage % camCount); _itemKeys[currentCamIndex]?.currentState?.refresh(); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( flex: 7, child: PageView.builder( controller: _pageController, itemBuilder: (context, index){ int camIndex = index % camCount; return CamItemPage( key: _itemKeys[camIndex], camNumber: camIndex +1, ); }, ), ), Expanded( flex: 1, child: Row( children: [ Expanded( child: GestureDetector( onTap: (){}, child: Container( decoration: BoxDecoration( color: Colors.indigo.shade300, border: Border.all(width: 2, color: Colors.white), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.location_on, size: 40,), Text('map') ], ) ) ), ), Expanded( child: GestureDetector( onTap: _refreshCurrentCam, child: Container( decoration: BoxDecoration( color: Colors.indigo.shade300, border: Border.all(width: 2, color: Colors.white), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.refresh, size: 35,), Text('reload') ], ) ) ), ), ], ) ) ], ); } }
方案2:使用ValueNotifier触发局部刷新
如果不想拆分组件,可以用ValueNotifier控制单个FutureBuilder的刷新,避免全局重绘。
class _CamScreen extends State<CamScreen>{ const camCount = 5; final _pageController = PageController(initialPage: camCount * 5); int _currentPage = camCount *5; // 为每个摄像头创建刷新触发器 final Map<int, ValueNotifier<int>> _refreshTriggers = {}; @override void initState() { super.initState(); for(int i=1; i<=camCount; i++){ _refreshTriggers[i] = ValueNotifier(0); } _pageController.addListener(() { setState(() { _currentPage = _pageController.page?.round() ?? camCount *5; }); }); } void _refreshCurrentCam() { int currentCamNumber = (_currentPage % camCount) +1; _refreshTriggers[currentCamNumber]?.value++; } Future<Map<String, dynamic>> fetchData(int camNumber) async { final response = await http.get(Uri.parse('https://mydomain/get_cam_info?no=$camNumber')); if (response.statusCode == 200) { return jsonDecode(response.body); } else { throw Exception('Failed to load data'); } } @override Widget build(BuildContext context) { return Column( children: [ Expanded( flex: 7, child: PageView.builder( controller: _pageController, itemBuilder: (context, index){ int camNumber = index % camCount +1; // 通过ValueListenableBuilder监听刷新触发 return ValueListenableBuilder( valueListenable: _refreshTriggers[camNumber]!, builder: (context, refreshCount, child) { return FutureBuilder( future: fetchData(camNumber), builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting){ return const Center(child: CircularProgressIndicator()); } else if(snapshot.hasError || snapshot.data == null){ return Column(children: [Expanded(child: Image.network('https://mydomain/cam_img?no=${camNumber}&cache=${refreshCount}',) )]); } else { final jsonData = snapshot.data!; final camName = jsonData['name']; final upperPath = jsonData['upper']['path']; final upperUrl = jsonData['upper']['url']; final lowerPath = jsonData['lower']['path']; final lowerUrl = jsonData['lower']['url']; return Scaffold( backgroundColor: Colors.black, appBar: AppBar( toolbarHeight: MediaQuery.of(context).size.height /16, title: Text(camName), centerTitle: true, backgroundColor: Colors.orange, ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (upperUrl != null && upperUrl.isNotEmpty && upperPath != null && upperPath.isNotEmpty) Expanded(child: GestureDetector( onTap: () async { await launchUrl(Uri.parse(upperUrl)); }, child: Image.network('https://mydomain/img/$upperPath?cache=${refreshCount}'), ),), SizedBox( width: MediaQuery.of(context).size.width, child:Image.network('https://mydomain/cam_img?no=${camNumber}&cache=${refreshCount}',)
相关产品推荐
相关产品推荐

