Search code examples
flutterflutter-imageflutter-pageview

How to reload only the Image retrieved from the URL in PageView


I am new to flutter. I want to create a screen to view images acquired from multiple surveillance cameras. I used a PageView to switch between camera images by swiping, and I also placed a button outside the PageView to reload the images. When this button is pressed, I want to reload only the camera image that is being displayed.The camera image has different images above and below it, but these do not need to be reloaded.

If I put setState() in the OnTap of the reload button, I could reload it, but the PageView would flicker. Is it possible to reload only the Image? Or is there another way? Thanks.

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')
                      ],
                    )
                  )
                ),
              ),
            ],
          )
        )
        
      ],
    );
  }
}

Solution

  • This could be achieved by separating Image.network into a separate widget.

    cam.dart

    import 'dart:convert';
    import 'dart:math';
    import 'package:flutter/material.dart';
    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) {
          return jsonDecode(response.body);
        } else {
          throw Exception('Failed to load data');
        }
      }
    
      @override
      Widget build(BuildContext context) {
        const camCount = 5;
        final _pageController = PageController(initialPage: camCount * 5);
        final List<GlobalKey<_ReloadableImageState>> _keys = List.generate(camCount*10, (index) => GlobalKey<_ReloadableImageState>());
    
        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 Column(children: [Expanded(child: CircularProgressIndicator())]);
                      } else if(snapshot.hasError || snapshot.data == null){
                        return Column(children: [Expanded(child:
                          ReloadableImage(key: _keys[index],camNumber: index % camCount + 1,)
                        )]);
                      } 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'),
                                ),),
                              
                              ReloadableImage(key: _keys[index],camNumber: index % camCount + 1),
    
                              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: GestureDetector(
                onTap: (){
                  final currentPage = _pageController.page?.round() ?? 0;
                  _keys[currentPage].currentState?.reloadImage();
                },
                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')
                    ],
                  )
                )
              ),
            ),
          ],
        );
      }
    }
    
    class ReloadableImage extends StatefulWidget {
      final int camNumber;
      final GlobalKey<_ReloadableImageState> key;
      ReloadableImage({required this.camNumber, required this.key}) : super(key: key);
    
      @override
      _ReloadableImageState createState() => _ReloadableImageState();
    }
    
    class _ReloadableImageState extends State<ReloadableImage> {
      void reloadImage() {
        setState(() {
        });
      }
      @override
      Widget build(BuildContext context) {
        return Image.network('https://mydomain/cam_img?no=${widget.camNumber}&cache=${Random().nextInt(10000)}');
      }
    }