Флаттер: жизненный цикл виджета и навигации
Я написал плагин флаттера, который отображает предварительный просмотр камеры и сканирует штрих-коды. у меня есть 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, который вы можете получить.