Gracefully resuming the camera in your flutter app
A lot of mobile apps rely on cameras, and as a cross platfor mible platform, flutter offers the camera plugin that provides APIs to interact with the camera. And as you would expect from this introductory sentence, I happen to be working on an app that uses a camera. For most of the part the camera API was working fine, except it stops working when I switch to a different app and then come back to the app with camera after some time. This seems to be a known problem (like this), and the high level solution is to register callbacks to the app's suspend/resume lifecycle that would dispose and initialize the camera accordingly. The trick is showcased in the example app in the camera plugin's directory, which I ran and verified that the camera would gracefully handle the resume. Back to my context, the solution in the example app was not a drop in solution because the app is build on riverpod using Providers and Consumer widgets it provides, instead of plain StatefulWidgets that the example app was using. So, the following provider for camera was implemented. code:dart
class CameraNotifier extends StateNotifier <AsyncValue<CameraController>>
with WidgetsBindingObserver {
CameraNotifier(ref): super(const AsyncValue.loading()) {
initialize(ref);
}
disposeCamera(){
state.value?.dispose();
state = const AsyncValue.loading();
}
// Only add observer once.
// the bounded widget is long living, but the camera can be
// initialized several times.
//
bool observerAdded = false;
@override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState lifeCycleState) {
final CameraController? cameraController = state.value;
// App state changed before we got the chance to initialize.
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (lifeCycleState == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (lifeCycleState == AppLifecycleState.resumed) {
initialize(null);
}
}
Future<void> initialize(WidgetRef? ref) async {
var cameras = await availableCameras();
if(!observerAdded){ //want only 1
WidgetsBinding.instance!.addObserver(this);
observerAdded = true;
}
if (cameras.isNotEmpty) {
if (kDebugMode) {
print(cameras.first);
}
final cameraController = CameraController(cameras.first, ResolutionPreset.max);
await cameraController.initialize();
state = await AsyncValue.guard(() async => cameraController);
}
}
}
final notifyingCameraProvider = StateNotifierProvider<CameraNotifier,AsyncValue<CameraController>>((ref) {
return CameraNotifier(ref);
});
and the widget will get access to the camera controller instance like
code:dart
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<CameraController?> camera = ref.watch(notifyingCameraProvider);
return Scaffold(
appBar: AppBar(
title: const Text("Title"),
),
drawer: _drawer(context, ref),
body: SafeArea(
child: Center(
child: camera.when(
loading: () => const SizedBox(
width: 128,
height: 128,
child: //show spinny,
error: (err, stack) => Text('Error: $err'),
data: (cameraController) {
return _body(context, ref, cameraController);
})
)),
)
Following are the points to note:
StateNotifierProvider was chosen expecting a need to operate on the state from the widget, but because the widgets simply pass down the camera controller object down the widget tree and interact with it directly, this was seemed unnecessary.
with WidgetsBindingObserver is the secret sause that allowed the couping of the state management with the suspend/resume lifecycle. Apparantly this is mixin, but my dart-fu is limited.