skip to Main Content

I am creating an app like snapchat where the app start with camera page and before this it ask user for camera and microphone permission if the user grant both permission then the camera widget will be displayed other wise instead of camera widget center text button is being displayed that open the app settings

Now the issue is if the user grant the permission and i navigate to other tabs or close and re-open the app i saw the text button instead of camera for like very short interval of micro-seconds
I don’t want this behavior if the permission is granted just show camera page at once if not granted then show the center text button

Anyone can tell me how to solve this:

Here is my CameraPreview class code:

 import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

class CameraaPreview extends StatefulWidget {
  final List<CameraDescription> cameras;
  const CameraaPreview(
      {super.key, required this.cameras, required this.permissionStatus});
  final bool permissionStatus;

  @override
  State<CameraaPreview> createState() => _CameraaPreviewState();
}

class _CameraaPreviewState extends State<CameraaPreview> {
  late CameraController _cameraController;
  Future<void>? _intializeFutureController;
  XFile? image;
  bool havePic = false;
  bool haveVide = false;
  bool _isRecording = false;
  late Timer _timer;
  bool isBackCameraOpen = true;
  int _recordedSeconds = 0;
  bool _permissionsGranted = false;

  @override
  void initState() {
    super.initState();
    requestCameraAndMicrophonePermissions().then((granted) {
      if (granted) {
        setState(() {
          _permissionsGranted = true;
          _cameraController = CameraController(
              widget.cameras[0], ResolutionPreset.max,
              imageFormatGroup: ImageFormatGroup.bgra8888);
          _intializeFutureController = _cameraController.initialize();
        });
      }
    });
  }

  @override
  void dispose() {
    _cameraController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final Size height = MediaQuery.of(context).size;
    const double navBarHeight = kBottomNavigationBarHeight;
    final avatarSize = height.width * 0.0999;
    final avataPostion = (navBarHeight - avatarSize) / 0.35;

    if (!_permissionsGranted) {
      return Center(
        child: TextButton(
          style: TextButton.styleFrom(backgroundColor: Colors.transparent),
          onPressed: () {
            openAppSettings();
          },
          child: const Text(
            'Grant Permissions for Camera and Microphone to continue',
            style: TextStyle(
              color: Colors.black,
            ),
          ),
        ),
      );
    }

    if (_permissionsGranted) {
      _permissionsGranted = true;
      _cameraController = CameraController(
          widget.cameras[0], ResolutionPreset.max,
          imageFormatGroup: ImageFormatGroup.bgra8888);
      _intializeFutureController = _cameraController.initialize();
    }

    return havePic
        ? _showImage(image)
        : RepaintBoundary(
            child: FutureBuilder(
              future: _intializeFutureController,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return const Center(
                    child: CircularProgressIndicator(
                      color: Colors.yellow,
                    ),
                  );
                } else if (snapshot.hasError) {
                  return Center(
                    child: Text('Error: ${snapshot.error}'),
                  );
                } else {
                  return Stack(
                    children: <Widget>[
                      SizedBox(
                        height: MediaQuery.of(context).size.height,
                        width: MediaQuery.of(context).size.width,
                        child: AspectRatio(
                          aspectRatio: _cameraController.value.aspectRatio,
                          child: CameraPreview(
                            _cameraController,
                          ),
                        ),
                      ),
                      Positioned(
                        bottom: avataPostion,
                        left: height.width / 2.25 - avatarSize / 2,
                        child: GestureDetector(
                          onLongPressStart: (_) => _startRecording(),
                          onLongPressEnd: (_) => _stopRecording(),
                          onTap: () => _clickPrecture(),
                          child: CircleAvatar(
                            radius: avatarSize,
                            backgroundColor: Colors.transparent,
                            child: Container(
                              decoration: BoxDecoration(
                                  border:
                                      Border.all(color: Colors.white, width: 4),
                                  borderRadius:
                                      BorderRadius.circular(avatarSize)),
                            ),
                          ),
                        ),
                      ),
                      Positioned(
                        top: 40,
                        left: height.width / 1.18,
                        child: GestureDetector(
                          onLongPressStart: (_) {
                            _startRecording();
                          },
                          onLongPressEnd: (_) {
                            _stopRecording();
                          },
                          child: IconButton(
                            onPressed: () {
                              setState(() {
                                isBackCameraOpen
                                    ? _openFrontCam()
                                    : _openBackCam();
                              });
                            },
                            icon: const Icon(
                              Icons.crop_rotate,
                              color: Colors.white,
                            ),
                          ),
                        ),
                      ),
                      if (_isRecording)
                        Positioned(
                          top: 20,
                          left: height.width / 2.25,
                          child: Text(
                            '$_recordedSeconds s',
                            style: const TextStyle(
                              color: Colors.red,
                              fontSize: 24,
                            ),
                          ),
                        ),
                    ],
                  );
                }
              },
            ),
          );
  }

  Future<bool> requestCameraAndMicrophonePermissions() async {
    Map<Permission, PermissionStatus> statuses = await [
      Permission.camera,
      Permission.microphone,
    ].request();

    return (statuses[Permission.camera] == PermissionStatus.granted &&
        statuses[Permission.microphone] == PermissionStatus.granted);
  }

  void _clickPrecture() async {
    if (_cameraController.value.isInitialized) {
      image = await _cameraController.takePicture();
      setState(() {
        havePic = !havePic;
      });
    }
  }

  void _startRecording() async {
    if (_cameraController.value.isInitialized) {
      _cameraController.startVideoRecording();
      _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
        setState(() {
          _recordedSeconds += 1;
        });
      });
      setState(() {
        _isRecording = true;
      });
    }
  }

  void _stopRecording() async {
    if (_cameraController.value.isRecordingVideo) {
      _timer.cancel();
      _recordedSeconds = 0;
      XFile videoFile = await _cameraController.stopVideoRecording();
      setState(() {
        _isRecording = false;
      });
    }
  }

  void _openFrontCam() {
    _cameraController = CameraController(
        widget.cameras[1], ResolutionPreset.max,
        imageFormatGroup: ImageFormatGroup.bgra8888);
    _intializeFutureController = _cameraController.initialize();
    isBackCameraOpen = !isBackCameraOpen;
  }

  void _openBackCam() {
    _cameraController = CameraController(
        widget.cameras[0], ResolutionPreset.max,
        imageFormatGroup: ImageFormatGroup.bgra8888);
    _intializeFutureController = _cameraController.initialize();
    isBackCameraOpen = !isBackCameraOpen;
  }

  _showImage(XFile? image) {
    return Stack(
      children: [
        SizedBox(
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          child: Image.file(
            File(image!.path),
            fit: BoxFit.cover,
          ),
        ),
      ],
    );
  }
}

here is my homePage class Code:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:ssnnaappcchhaatt/ios_screens/camera_preview.dart';
import 'package:ssnnaappcchhaatt/ios_screens/chat_page.dart';
import 'package:ssnnaappcchhaatt/ios_screens/map_page.dart';
import 'package:ssnnaappcchhaatt/ios_screens/play_page.dart';
import 'package:ssnnaappcchhaatt/ios_screens/search_page.dart';
import 'package:ssnnaappcchhaatt/ios_screens/stories_page.dart';

class HomePage extends StatefulWidget {
  final List<CameraDescription> cameras;
  final bool permissionStatus;
  static const homePage = 'homePage/';
  const HomePage(
      {super.key, required this.cameras, required this.permissionStatus});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int currentIndex = 2;
  bool cameraShow = true;

  @override
  Widget build(BuildContext context) {
    final screens = [
      const MapPage(),
      const ChatPage(),
      !cameraShow
          ? const SearchPage()
          : CameraaPreview(
              cameras: widget.cameras,
              permissionStatus: widget.permissionStatus,
            ),
      const StoriesPage(),
      const PlayPage()
    ];
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 169, 154, 154),
      body: screens[currentIndex],
      bottomNavigationBar: BottomNavigationBar(
          items: [
            const BottomNavigationBarItem(
              label: "Map",
              icon: Icon(
                CupertinoIcons.map_pin,
                size: 24,
              ),
            ),
            const BottomNavigationBarItem(
              label: "Chat",
              icon: Icon(
                CupertinoIcons.chat_bubble,
                size: 24,
              ),
            ),
            BottomNavigationBarItem(
              label: !cameraShow ? "Camera" : "Search",
              icon: !cameraShow
                  ? GestureDetector(
                      onTap: () => {
                        Navigator.of(context)
                            .pushReplacementNamed(HomePage.homePage)
                      },
                      child: const Icon(
                        CupertinoIcons.camera,
                        size: 24,
                      ),
                    )
                  : const Icon(
                      CupertinoIcons.search,
                      size: 24,
                    ),
            ),
            const BottomNavigationBarItem(
              label: "Stories",
              icon: Icon(
                CupertinoIcons.group,
                size: 30,
              ),
            ),
            const BottomNavigationBarItem(
              label: "play",
              icon: Icon(
                CupertinoIcons.play,
                size: 24,
              ),
            )
          ],
          type: BottomNavigationBarType.fixed,
          showSelectedLabels: false,
          showUnselectedLabels: false,
          backgroundColor: Colors.transparent,
          unselectedItemColor: Colors.black,
          selectedItemColor: Colors.blue,
          currentIndex: currentIndex,
          onTap: (value) {
            if (value == 2 || currentIndex == 2) {
              setState(() {
                currentIndex = value;
                cameraShow = !cameraShow;
              });
            } else {
              setState(() {
                currentIndex = value;
              });
            }
          }),
    );
  }
}

here is my main class code:

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';

import 'package:ssnnaappcchhaatt/ios_screens/home_page.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final cameras = await availableCameras();
  runApp(MyApp(
    cameras: cameras,
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.cameras});
  final List<CameraDescription> cameras;
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Snap Chat',
      debugShowCheckedModeBanner: false,
      home: HomePage(
        cameras: cameras,
        permissionStatus: false,
      ),
      routes: {
        HomePage.homePage: (context) => HomePage(
              cameras: cameras,
              permissionStatus: false,
            ),
      },
    );
  }
}

2

Answers


  1. You need to add condition for atleast 3 states of FutureBuilder here

    1. Waiting for data
    2. Received Data
    3. Error receiving data

    In your implementation I see only two. Number 2 and 3. There is no condition for 1. So while your FutureBuilder is waiting for the data, you see the TextButton as well.

    You could try with the below minimalist logic:

         if (snapshot.hasData) {
                 //Return your main widget here
                 //Add a conditional here based on data to show either Camera or TextButton
             } else if (snapshot.hasError) {
                  //Return your error widget here
             } else {
                 //Return your waiting for data widget here
             }
    
    Login or Signup to reply.
  2. The issue you’re facing is because the permission check is being done every time the build method is called, which happens every time the widget is rebuilt, for example, when navigating between tabs or opening the app again.

    To avoid this behavior, you can move the permission check to the initState method, which is only called once when the widget is first created. This way, the permission check will only be done once when the widget is created, and the camera preview will be shown immediately if the permission is granted.

    Here’s an updated version of your code with the permission check moved to initState:

    class CameraaPreview extends StatefulWidget {
      final List<CameraDescription> cameras;
      const CameraaPreview({super.key, required this.cameras});
    
      @override
      State<CameraaPreview> createState() => _CameraaPreviewState();
    }
    
    class _CameraaPreviewState extends State<CameraaPreview> {
      late CameraController _cameraController;
      late Future<void> _intializeFutureController;
      XFile? image;
      bool havePic = false;
      bool haveVide = false;
      bool _isRecording = false;
      late Timer _timer;
      bool isBackCameraOpen = true;
      int _recordedSeconds = 0;
      bool _permissionsGranted = false;
    
      @override
      void initState() {
        super.initState();
        requestCameraAndMicrophonePermissions().then((granted) {
          if (granted) {
            setState(() {
              _permissionsGranted = true;
              _cameraController = CameraController(
                  widget.cameras[0], ResolutionPreset.max,
                  imageFormatGroup: ImageFormatGroup.bgra8888);
              _intializeFutureController = _cameraController.initialize();
            });
          }
        });
      }
    
      @override
      void dispose() {
        _cameraController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final Size height = MediaQuery.of(context).size;
        const double navBarHeight = kBottomNavigationBarHeight;
        final avatarSize = height.width * 0.0999;
        final avataPostion = (navBarHeight - avatarSize) / 0.35;
    
        if (!_permissionsGranted) {
          return Center(
            child: TextButton(
              style: TextButton.styleFrom(backgroundColor: Colors.transparent),
              onPressed: () {
                openAppSettings();
              },
              child: const Text(
                'Grant Permissions',
                style: TextStyle(color: Colors.black, fontSize: 25),
              ),
            ),
          );
        }
    
        return havePic
            ? _showImage(image)
            : FutureBuilder(
                future: _intializeFutureController,
                builder: (context, snapshot) {
                  return Stack(
                    children: <Widget>[
                      SizedBox(
                        height: MediaQuery.of(context).size.height,
                        width: MediaQuery.of(context).size.width,
                        child: AspectRatio(
                          aspectRatio: _cameraController.value.aspectRatio,
                          child: CameraPreview(
                            _cameraController,
                          ),
                        ),
                      ),
                      Positioned(
                        bottom: avataPostion,
                        left: height.width / 2.25 - avatarSize / 2,
                        child: GestureDetector(
                          onLongPressStart: (_) => _startRecording(),
                          onLongPressEnd: (_) => _stopRecording(),
                          onTap: () => _clickPrecture(),
                          child: CircleAvatar(
                            radius: avatarSize,
                            backgroundColor: Colors.transparent,
                            child: Container(
                              decoration: BoxDecoration(
                                  border:
                                      Border.all(color: Colors.white, width: 4),
                                  borderRadius:
                                      BorderRadius.circular(avatarSize)),
                            ),
                          ),
    

    By refactoring your code, you have removed the extra and unnecessary FutureBuilder as well. The way to solve use issue in future is to use a break point and check your flow. You will easily find your requestCameraAndMicrophonePermissions() called multiple times and thus it results in rebuild and those flashy events 😉

    UPDATE


    The error "Null check operator used on a null value" occurs because the FutureBuilder is trying to build the UI before the future has completed. This can happen when the _intializeFutureController is null at the time the FutureBuilder is trying to build the UI.

    To fix this, you can add a check to see if _intializeFutureController is null before building the UI. If it is null, you can return a loading indicator or an empty container until the future completes.

    Here’s an example:

    FutureBuilder(
      future: _intializeFutureController,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          // Future has not completed yet, show a loading indicator
          return CircularProgressIndicator();
        } else if (snapshot.hasError) {
          // Future has completed with an error, show an error message
          return Text('Error: ${snapshot.error}');
        } else {
          // Future has completed successfully, build the UI
          return Stack(
            children: <Widget>[
              SizedBox(
                height: MediaQuery.of(context).size.height,
                width: MediaQuery.of(context).size.width,
                child: AspectRatio(
                  aspectRatio: _cameraController.value.aspectRatio,
                  child: CameraPreview(
                    _cameraController,
                  ),
                ),
              ),
              // ...
            ],
          );
        }
      },
    ),
    

    This code checks if snapshot.hasData is false, which means that the future has not completed yet. In this case, it returns a CircularProgressIndicator widget to show a loading indicator. If snapshot.hasData is true, it checks snapshot.hasError to see if the future completed with an error. If so, it returns a Text widget with the error message. Otherwise, it builds the UI with the CameraPreview widget and other elements.

    As for the issue with the red error screen showing up briefly, it’s likely because the UI is being rebuilt when the camera is switched. To prevent this, you can try wrapping the FutureBuilder widget in a RepaintBoundary widget. This will prevent the parent widgets from being rebuilt when the child changes.

    Another Version Answer

    As I don’t have idea of what CameraDescription call is, I tried to make changes to the original answer. See if it works for you:

    import 'dart:async';
    import 'dart:io';
    
    import 'package:camera/camera.dart';
    import 'package:flutter/material.dart';
    import 'package:permission_handler/permission_handler.dart';
    
    class CameraMicPreview extends StatefulWidget {
      final List<CameraDescription> cameras;
      const CameraMicPreview({super.key, required this.cameras});
    
      @override
      State<CameraMicPreview> createState() => _CameraMicPreviewState();
    }
    
    class _CameraMicPreviewState extends State<CameraMicPreview> {
      late CameraController _cameraController;
      late Future<void> _intializeFutureController;
      XFile? image;
      bool havePic = false;
      bool haveVide = false;
      bool _isRecording = false;
      late Timer _timer;
      bool isBackCameraOpen = true;
      int _recordedSeconds = 0;
    
      @override
      void initState() {
        _cameraController = CameraController(
            widget.cameras[0], ResolutionPreset.max,
            imageFormatGroup: ImageFormatGroup.bgra8888);
        _intializeFutureController = _cameraController.initialize();
        super.initState();
      }
    
      @override
      void dispose() {
        _cameraController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final Size height = MediaQuery.of(context).size;
        const double navBarHeight = kBottomNavigationBarHeight;
        final avatarSize = height.width * 0.0999;
        final avataPostion = (navBarHeight - avatarSize) / 0.35;
    
        return havePic
            ? _showImage(image)
            : FutureBuilder(
                future: requestCameraAndMicrophonePermissions(),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    // Future has not completed yet, show a loading indicator
                    return const CircularProgressIndicator();
                  } else if (snapshot.hasError) {
                    // Future has completed with an error, show an error message
                    return Text('Error: ${snapshot.error}');
                  } else if (snapshot.hasData) {
                    return FutureBuilder(
                      future: _intializeFutureController,
                      builder: (context, snapshot) {
                        return Stack(
                          children: <Widget>[
                            SizedBox(
                              height: MediaQuery.of(context).size.height,
                              width: MediaQuery.of(context).size.width,
                              child: AspectRatio(
                                aspectRatio: _cameraController.value.aspectRatio,
                                child: CameraPreview(
                                  _cameraController,
                                ),
                              ),
                            ),
                            Positioned(
                              bottom: avataPostion,
                              left: height.width / 2.25 - avatarSize / 2,
                              child: GestureDetector(
                                onLongPressStart: (_) => _startRecording(),
                                onLongPressEnd: (_) => _stopRecording(),
                                onTap: () => _clickPrecture(),
                                child: CircleAvatar(
                                  radius: avatarSize,
                                  backgroundColor: Colors.transparent,
                                  child: Container(
                                    decoration: BoxDecoration(
                                        border: Border.all(
                                            color: Colors.white, width: 4),
                                        borderRadius:
                                            BorderRadius.circular(avatarSize)),
                                  ),
                                ),
                              ),
                            ),
                            Positioned(
                              top: 40,
                              left: height.width / 1.18,
                              child: GestureDetector(
                                onLongPressStart: (_) {
                                  _startRecording();
                                },
                                onLongPressEnd: (_) {
                                  _stopRecording();
                                },
                                child: IconButton(
                                  onPressed: () {
                                    setState(() {
                                      isBackCameraOpen
                                          ? _openFrontCam()
                                          : _openBackCam();
                                    });
                                  },
                                  icon: const Icon(
                                    Icons.crop_rotate,
                                    color: Colors.white,
                                  ),
                                ),
                              ),
                            ),
                            if (_isRecording)
                              Positioned(
                                top: 20,
                                left: height.width / 2.25,
                                child: Text(
                                  '$_recordedSeconds s',
                                  style: const TextStyle(
                                    color: Colors.red,
                                    fontSize: 24,
                                  ),
                                ),
                              ),
                          ],
                        );
                      },
                    );
                  }
                  return Center(
                    child: TextButton(
                      style:
                          TextButton.styleFrom(backgroundColor: Colors.transparent),
                      onPressed: () {
                        openAppSettings();
                      },
                      child: const Text(
                        'Grant Permissions',
                        style: TextStyle(color: Colors.black, fontSize: 25),
                      ),
                    ),
                  );
                },
              );
      }
    
      Future<bool> requestCameraAndMicrophonePermissions() async {
        Map<Permission, PermissionStatus> statuses = await [
          Permission.camera,
          Permission.microphone,
        ].request();
    
        return (statuses[Permission.camera] == PermissionStatus.granted &&
            statuses[Permission.microphone] == PermissionStatus.granted);
      }
    
      void _clickPrecture() async {
        if (_cameraController.value.isInitialized) {
          image = await _cameraController.takePicture();
          setState(() {
            havePic = !havePic;
          });
        }
      }
    
      void _startRecording() async {
        if (_cameraController.value.isInitialized) {
          _cameraController.startVideoRecording();
          _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
            setState(() {
              _recordedSeconds += 1;
            });
          });
          setState(() {
            _isRecording = true;
          });
        }
      }
    
      void _stopRecording() async {
        if (_cameraController.value.isRecordingVideo) {
          _timer.cancel();
          _recordedSeconds = 0;
          XFile videoFile = await _cameraController.stopVideoRecording();
          setState(() {
            _isRecording = false;
          });
        }
      }
    
      void _openFrontCam() {
        _cameraController = CameraController(
            widget.cameras[1], ResolutionPreset.max,
            imageFormatGroup: ImageFormatGroup.bgra8888);
        _intializeFutureController = _cameraController.initialize();
        isBackCameraOpen = !isBackCameraOpen;
      }
    
      void _openBackCam() {
        _cameraController = CameraController(
            widget.cameras[0], ResolutionPreset.max,
            imageFormatGroup: ImageFormatGroup.bgra8888);
        _intializeFutureController = _cameraController.initialize();
        isBackCameraOpen = !isBackCameraOpen;
      }
    
      _showImage(XFile? image) {
        return Stack(
          children: [
            SizedBox(
              height: MediaQuery.of(context).size.height,
              width: MediaQuery.of(context).size.width,
              child: Image.file(
                File(image!.path),
                fit: BoxFit.cover,
              ),
            ),
          ],
        );
      }
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search