Флаттер: жизненный цикл виджета и навигации

Я написал плагин флаттера, который отображает предварительный просмотр камеры и сканирует штрих-коды. у меня есть Widget называется ScanPage который отображает CameraPreview и переходит на новый Route когда штрих-код обнаружен.

Проблема: когда я нажимаю новый маршрут (SearchProductPage) в стек навигации, CameraController продолжает обнаруживать штрих-коды. Мне нужно позвонить stop() на моем CameraController когда ScanPage удаляется с экрана. Мне нужно позвонить start() снова, когда пользователь возвращается к ScanPage,

Что я пробовал:CameraController инвентарь WidgetsBindingObserver и реагирует на didChangeAppLifecycleState(), Это прекрасно работает, когда я нажимаю кнопку домой, но не когда я нажимаю новый Route в стек навигации.

Вопрос: есть ли эквивалент для viewDidAppear() а также viewWillDisappear() на iOS или onPause() а также onResume() на Android для Widgets во флаттере? Если нет, как я могу начать и остановить CameraController чтобы он прекратил сканирование штрих-кодов, когда другой виджет находится сверху стека навигации?

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

Редактировать:

/// 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();
      });
    }
  }
}

4 ответа

Решение

Я остановил controller прежде чем перейти к другой странице и перезапустить ее, когда pop() называется.

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

}

Другое решение было бы установить maintainState собственность route что открывается ScanPage в false,

Спасибо, это супер-полезно!

Мне также нужен был эквивалент ViewDidAppear. В итоге я взял отсюда "возобновленное" состояние, а затем поставил галочку в функции сборки.

Это означает, что моя проверка будет вызываться, когда приложение вернется на передний план и когда оно будет загружено.

Конечно, необходимо установить логическое значение, чтобы убедиться, что Build-Check вызывается только ОДИН РАЗ, а не каждый раз при перезагрузке представления.

Для моего приложения я действительно хотел, чтобы это происходило только один раз в день, но это можно легко адаптировать, чтобы это происходило один раз при загрузке приложения. Проверяемое логическое значение затем должно быть сброшено, когда приложение приостанавливается / закрывается.

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

Возможно, вы можете переопределить dispose метод для вашего виджета и заставить ваш контроллер остановиться внутри него. AFAIK, это был бы хороший способ справиться с этим, поскольку флаттер "автоматически" останавливал бы его каждый раз, когда вы располагаете виджет, поэтому вам не нужно следить за тем, когда запускать или останавливать камеру.

Кстати, мне нужен сканер штрих-кода /QR-кода с предварительным просмотром в реальном времени. Не могли бы вы поделиться своим плагином на git (или zip)?

Вы также можете использовать пакет FocusDetector, который ближе всего к viewDidAppear и onResume, который вы можете получить.

https://pub.dev/packages/focus_detector

Другие вопросы по тегам