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
You need to add condition for atleast 3 states of FutureBuilder here
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 theTextButton
as well.You could try with the below minimalist logic:
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
: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 abreak point
and check your flow. You will easily find yourrequestCameraAndMicrophonePermissions()
called multiple times and thus it results in rebuild and thoseflashy 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 theFutureBuilder
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:
This code checks if
snapshot.hasData
is false, which means that the future has not completed yet. In this case, it returns aCircularProgressIndicator
widget to show a loading indicator. Ifsnapshot.hasData
is true, it checkssnapshot.hasError
to see if the future completed with an error. If so, it returns aText
widget with the error message. Otherwise, it builds the UI with theCameraPreview
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 aRepaintBoundary
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: