Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Lifecycle of a Widget and Navigation

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();
      });
    }
  }
}
like image 317
hendra Avatar asked May 03 '18 12:05

hendra


People also ask

What is life cycle of widget Flutter?

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.

What is the lifecycle of stateful widget?

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.

How does widget work in 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”.


1 Answers

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.

like image 171
hendra Avatar answered Sep 30 '22 02:09

hendra