I am using the camera package for a simple functionality. I am mostly following the example the package provided. When I open the camera widget page, the package automatically prompts to provide permission to the camera and microphone. After clicking allow to both the permissions the debugger is paused with an exception:
Exception has occurred.
FlutterError (A CameraController was used after being disposed.
Once you have called dispose() on a CameraController, it can no longer be used.).
Here is the required code :
class CameraPage extends StatefulWidget {
@override
_CameraPageState createState() => _CameraPageState();
}
class _CameraPageState extends State<CameraPage>
with WidgetsBindingObserver {
CameraController _controller;
List<CameraDescription> _availableCameras;
...
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initialize();
}
Future<void> _initialize() async {
await _getCameras();
_controller = CameraController(_availableCameras[0], ResolutionPreset.high);
await _controller.initialize();
if (!mounted) {
return;
}
setState(() {});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
_controller?.dispose();
} else if (state == AppLifecycleState.resumed) {
if (_controller != null) {
_setCurrentCamera(_controller.description);
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose();
super.dispose();
}
Future<List<CameraDescription>> _getCameras() async {
List<CameraDescription> camDescriptions;
camDescriptions = await availableCameras();
_availableCameras = camDescriptions;
return camDescriptions;
}
@override
Widget build(BuildContext context) {
...
}
Future<void> _setCurrentCamera(CameraDescription cameraDescription) async {
if (_controller != null) {
await _controller.dispose();
}
_controller = CameraController(
cameraDescription,
ResolutionPreset.high,
enableAudio: false,
);
// If the _controller is updated then update the UI.
_controller.addListener(() {
if (mounted) setState(() {});
if (_controller.value.hasError) {
print('Camera error ${_controller.value.errorDescription}');
}
});
try {
await _controller.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void _switchCamera() {
if (_controller != null && !_controller.value.isRecordingVideo) {
CameraLensDirection direction = _controller.description.lensDirection;
CameraLensDirection required = direction == CameraLensDirection.front
? CameraLensDirection.back
: CameraLensDirection.front;
for (CameraDescription cameraDescription in _availableCameras) {
if (cameraDescription.lensDirection == required) {
_setCurrentCamera(cameraDescription);
return;
}
}
}
}
void _showCameraException(CameraException e) {
String errorText = 'Error: ${e.code}\nError Message: ${e.description}';
print(errorText);
}
}
The debugger is pointing the exception here:
Future<void> _initialize() async {
await _getCameras();
_controller = CameraController(_availableCameras[0], ResolutionPreset.high);
//-------------HERE------------------
await _controller.initialize();
if (!mounted) {
return;
}
setState(() {});
}
Once I resume the debugger and try opening this camera page again there is no error/exception anymore. It Is happening only after accepting permissions for the first time.
May be the actual culprit is didChangeAppLifecycleState
.
Once you call await _controller.initialize();
and the permission dialog is shown, the lifecycle event AppLifecycleState.inactive
is triggered and current controller is disposed as per your code in didChangeAppLifecycleState
, hence when the application resumes after permissions given and tries to continue, it throws error.
Try removing
if (state == AppLifecycleState.inactive) {
_controller?.dispose();
}
Or have a local variable to check if initializing and ignore dispose when initiliazing like
Future<void> _initialize() async {
await _getCameras();
_controller = CameraController(_availableCameras[0], ResolutionPreset.high);
_initializing = true;
await _controller.initialize();
_initializing = false;
if (!mounted) {
return;
}
setState(() {});
}
and in didChangeAppLifecycleState
if (state == AppLifecycleState.inactive && !_initializing) {
_controller?.dispose();
}
EDIT:
May be, I think I found the issue, the actual issue is didChangeAppLifecycleState
as expected, the if
clause in the didChangeAppLifecycleState
, if it truns out to be true, _controller
is being disposed, if not _setCurrentCamera
is just disposing any active controller. Hence when you invoke initialize and wait for permissions, before permission future resolves, the _controller
is being disposed by didChangeAppLifecycleState
.
My solution would work with simple change. Change your initState
to
@override
void initState() {
super.initState();
_initializing = true; // set to true
WidgetsBinding.instance.addObserver(this);
_initialize();
}
change your _initialize
function to make _initializing = false
after intializing like,
Future<void> _initialize() async {
await _getCameras();
_controller = CameraController(_availableCameras[0],ResolutionPreset.high);
await _controller.initialize();
_initializing = false; // set to false
if (!mounted) {
return;
}
setState(() {});
}
and your didChangeAppLifecycleState
to
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(_initializing){
return;
}
if (state == AppLifecycleState.inactive) {
_controller?.dispose();
} else if (state == AppLifecycleState.resumed) {
if (_controller != null) {
_setCurrentCamera(_controller.description);
}
}
}
This way, if _initializing == true
you never dispose the current controller.
Hope that helps!