Reputation: 2651
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();
});
}
}
}
Upvotes: 18
Views: 14987
Reputation: 2820
You can also use the FocusDetector package which is the closest thing to ‘viewDidAppear’ and ‘onResume’ that you can get.
https://pub.dev/packages/focus_detector
Upvotes: 0
Reputation: 1013
Thanks, this is super-useful!
I also needed an equivalent of ViewDidAppear
. What I ended up doing was taking the "resumed" state from here, and then also putting a check in the Build-function.
That means my check would be called when the app came back to the foreground, and when it would be loaded.
Of course there needs to be boolean set to make sure the Build-Check would only be called ONCE, and not every time the view is reloading.
For my app I actually wanted it to only happen once per day, but this could easily be adapted to happen once per app-load. The boolean that is checked would then have to be reset when the app is suspended / quit.
(pseudocode, greatly reduced, still in progress)
bool hasRunViewDidAppearThisAppOpening = false;
@override
Widget build(BuildContext context) {
_viewDidAppear();
...
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
_viewDidAppear();
} else {
hasRunViewDidAppearThisAppOpening = false;
}
}
Future<void> _viewDidAppear() async {
if (!hasRunViewDidAppearThisAppOpening) {
hasRunViewDidAppearThisAppOpening = true;
// Do your _viewDidAppear code here
}
}
Upvotes: 0
Reputation: 2964
Perhaps you can override the dispose
method for your widget and get your controller to stop inside it. AFAIK, that would be a nice way handle it since flutter would 'automatically' stop it every time you dispose the widget, so you don't have to keep tabs yourself on when to start or stop the camera.
By the way, I am in need of a barcode/QR-code scanner with a live preview. Would you mind sharing your plugin on git (or zip)?
Upvotes: 0
Reputation: 2651
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
.
Upvotes: 13