You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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}',)
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 23:47:34