Flutter: Lifecycle of a Widget and Navigation
Asked Answered
P

4

18

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();
      });
    }
  }
}
Powell answered 3/5, 2018 at 12:33 Comment(3)
Can you add a part of your CameraController ?Firedog
@RémiRousselet: Yes, I added the implementation of CameraController. It's heavily inspired by flutter's Camera Plugin.Powell
@RémiRousselet Any idea on that? Is it a bad approach?Powell
P
13

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.

Powell answered 8/5, 2018 at 22:40 Comment(0)
N
0

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)?

Nottinghamshire answered 11/5, 2018 at 19:45 Comment(0)
S
0

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
  }
}

Stoltz answered 21/4, 2020 at 13:13 Comment(0)
W
0

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

Wellgrounded answered 10/6, 2021 at 15:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.