I have written a flutter plugin, that displays a camera preview and scans for barcodes. I have a Widget
called ScanPage
that displays the CameraPreview
and navigates to a new Route
when a barcode is detected.
Problem:
When I push a new Route (SearchProductPage
) to the navigation stack, the CameraController
continues to detect barcodes. I need to call stop()
on my CameraController
when the ScanPage
is removed from the screen. I need to call start()
again, when the user returns to the ScanPage
.
What I tried:
The CameraController
implements WidgetsBindingObserver
and reacts to didChangeAppLifecycleState()
. This works perfectly when I press the home button, but not when I push a new Route
to the navigation stack.
Question:
Is there an equivalent for viewDidAppear()
and viewWillDisappear()
on iOS or onPause()
and onResume()
on Android for Widgets
in Flutter? If not, how can I start and stop my CameraController
so that it stops scanning for barcodes when another Widget is on top of the navigation stack?
class ScanPage extends StatefulWidget {
ScanPage({ Key key} ) : super(key: key);
@override
_ScanPageState createState() => new _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
//implements WidgetsBindingObserver
CameraController controller;
@override
void initState() {
controller = new CameraController(this.didDetectBarcode);
WidgetsBinding.instance.addObserver(controller);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
//navigate to new page
void didDetectBarcode(String barcode) {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (BuildContext buildContext) {
return new SearchProductPage(barcode);
},
)
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(controller);
controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!controller.value.initialized) {
return new Center(
child: new Text("Lade Barcodescanner..."),
);
}
return new CameraPreview(controller);
}
}
Edit:
/// Controls a device camera.
///
///
/// Before using a [CameraController] a call to [initialize] must complete.
///
/// To show the camera preview on the screen use a [CameraPreview] widget.
class CameraController extends ValueNotifier<CameraValue> with WidgetsBindingObserver {
int _textureId;
bool _disposed = false;
Completer<Null> _creatingCompleter;
BarcodeHandler handler;
CameraController(this.handler) : super(const CameraValue.uninitialized());
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch(state){
case AppLifecycleState.inactive:
print("--inactive--");
break;
case AppLifecycleState.paused:
print("--paused--");
stop();
break;
case AppLifecycleState.resumed:
print("--resumed--");
start();
break;
case AppLifecycleState.suspending:
print("--suspending--");
dispose();
break;
}
}
/// Initializes the camera on the device.
Future<Null> initialize() async {
if (_disposed) {
return;
}
try {
_creatingCompleter = new Completer<Null>();
_textureId = await BarcodeScanner.initCamera();
print("TextureId: $_textureId");
value = value.copyWith(
initialized: true,
);
_applyStartStop();
} on PlatformException catch (e) {
value = value.copyWith(errorDescription: e.message);
throw new CameraException(e.code, e.message);
}
BarcodeScanner._channel.setMethodCallHandler((MethodCall call){
if(call.method == "barcodeDetected"){
String barcode = call.arguments;
debounce(2500, this.handler, [barcode]);
}
});
_creatingCompleter.complete(null);
}
void _applyStartStop() {
if (value.initialized && !_disposed) {
if (value.isStarted) {
BarcodeScanner.startCamera();
} else {
BarcodeScanner.stopCamera();
}
}
}
/// Starts the preview.
///
/// If called before [initialize] it will take effect just after
/// initialization is done.
void start() {
value = value.copyWith(isStarted: true);
_applyStartStop();
}
/// Stops the preview.
///
/// If called before [initialize] it will take effect just after
/// initialization is done.
void stop() {
value = value.copyWith(isStarted: false);
_applyStartStop();
}
/// Releases the resources of this camera.
@override
Future<Null> dispose() {
if (_disposed) {
return new Future<Null>.value(null);
}
_disposed = true;
super.dispose();
if (_creatingCompleter == null) {
return new Future<Null>.value(null);
} else {
return _creatingCompleter.future.then((_) async {
BarcodeScanner._channel.setMethodCallHandler(null);
await BarcodeScanner.disposeCamera();
});
}
}
}
The life cycle of stateless widgets is simple; there's only one stage: the build method. As soon as the widget gets built, the build method gets automatically called where you are supposed to create whatever appearance you want to add up in your application.
The lifecycle of a stateful Widget explains the calling hierarchy of functions and changes in the state of the widget. Everything in the flutter is a widget. Basically, the Stateful and stateless are the types of widget in the flutter.
Flutter provides a number of widgets that help you build apps that follow Material Design. A Material app starts with the MaterialApp widget, which builds a number of useful widgets at the root of your app, including a Navigator , which manages a stack of widgets identified by strings, also known as “routes”.
I ended up stopping the controller
before I navigate to the other page and restart it, when pop()
is called.
//navigate to new page
void didDetectBarcode(String barcode) {
controller.stop();
Navigator.of(context)
.push(...)
.then(() => controller.start()); //future completes when pop() returns to this page
}
Another solution would be to set the maintainState
property of the route
that opens ScanPage
to false
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With