UI se congela en simple computación asinc

Contexto

Estoy trabajando en una función que ejecutará algunas acciones cronológicamente y quiero ser capaz de pausar su ejecución y luego reanudar en una pulsación de botón.

Problema

Con el siguiente código, la interfaz de usuario se congela cuando el código llega al await Future.doWhile(() => _isTestPaused);

Código

import 'package:flutter/material.dart';

class Sample extends StatefulWidget {
  const Sample();

  @override
  _SampleState createState() => _SampleState();
}

class _SampleState extends State {
  String _text;
  bool _isTestPaused;

  @override
  void initState() {
    _text = '';
    _isTestPaused = false;
    WidgetsBinding.instance.addPostFrameCallback((_) => _run());
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Expanded(child: Text(_text)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _isTestPaused = !_isTestPaused),
        child: Icon(_isTestPaused ? Icons.play_arrow : Icons.pause),
      ),
    );
  }

  Future _run() async {
    int count = 0;

    for (int i = 0; i < 100; i++) {
      await Future.delayed(const Duration(seconds: 1));
      await Future.doWhile(() => _isTestPaused);
      setState(() => _text += '${count += 1}\n');
    }
  }
}

Pregunta hecha hace 3 años, 4 meses, 29 días - Por codercentaur


4 Respuestas:

  • Estás bloqueando tu código en await Future.doWhile De hecho estarías bloqueando incluso si no estuvieras usando await Esto es porque el dardo es un solo hilo.

    Cuando llegas a un dardo de función asincrónica no crea un hilo separado, simplemente pasa la función, y cada vez que está listo cuenta el bucle del evento y se para en la parte posterior de la línea. Muy similar a cómo Nodejs maneja las cosas.

    Puede abarcar un aislato para ejecutar operaciones costosas utilizando compute dentro dart:asyncPero no sé si puedes detener la ejecución de un bucle cambiando algún estado. Pero lo que sea que estés atando para hacer esto probablemente no es la manera correcta.

    Buena suerte.

    Respondida el Dec 16, 2020 a las 16:22 - por bytebarricade

    Votos positivos: 0 | Votos negativos: 0

  • Utilice un método Future para ejecutar el código async. Entonces llama a usar Future Builder. Dado que el desbordamiento es un solo hilo, llamando a async directamente desde dentro, puede hacer que la aplicación se congele.

    Respondida el Dec 16, 2020 a las 16:27 - por scriptwarrior

    Votos positivos: 0 | Votos negativos: 0

  • El problema que estás experimentando con la congelación de la interfaz de usuario se debe a que estás utilizando Future.doWhile() de forma sincrónica en tu función _run(). Esto significa que mientras estás esperando a que _isTestPaused cambie en el bucle doWhile, estás bloqueando el hilo principal de Flutter, lo que provoca la congelación de la UI.

    Para solucionar este problema, puedes modificar tu función _run() para hacer un uso más eficiente de la asincronía, utilizando un Future que se resuelva cuando _isTestPaused cambie. A continuación te muestro un ejemplo de cómo podrías refactorizar tu código para evitar que la UI se congele:

    dart
    Future _run() async {
      int count = 0;
    
      for (int i = 0; i < 100; i++) {
        await Future.delayed(const Duration(seconds: 1));
    
        // Cambia el bucle doWhile a un Future que se resuelve cuando _isTestPaused cambie
        await _waitForUnpaused();
    
        setState(() {
          _text += '${count += 1}\n';
        });
      }
    }
    
    Future _waitForUnpaused() {
      if (!_isTestPaused) {
        return Future.value();
      }
    
      Completer completer = Completer();
    
      void onResume() {
        if (!_isTestPaused) {
          completer.complete();
        } else {
          completer.complete(_waitForUnpaused());
        }
      }
    
      _isTestPaused ? completer.future.then((_) => onResume()) : onResume();
    
      return completer.future;
    }
    

    En este código, la función _waitForUnpaused() se encarga de esperar de forma asincrónica hasta que _isTestPaused deje de ser true. Esto evita el bloqueo del hilo principal y permite que la UI siga siendo receptiva mientras la función _run() espera a que se reanude la ejecución.

    Respondida el Dec 17, 2020 a las 16:19 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • El problema con el código proporcionado es que el await Future.doWhile(() => _isTestPaused); bloquea el hilo de la interfaz de usuario, lo que hace que la interfaz de usuario se congele hasta que se complete el Future. Para evitar este problema, puedes utilizar un Stream para escuchar los cambios en _isTestPaused y pausar la ejecución cuando sea necesario. Aquí tienes un ejemplo de cómo puedes hacerlo:

    
    import 'package:flutter/material.dart';
    import 'package:rxdart/rxdart.dart';
    
    class Sample extends StatefulWidget {
      const Sample();
    
      @override
      _SampleState createState() => _SampleState();
    }
    
    class _SampleState extends State {
      String _text;
      bool _isTestPaused;
      StreamSubscription _pauseSubscription;
    
      @override
      void initState() {
        _text = '';
        _isTestPaused = false;
        WidgetsBinding.instance.addPostFrameCallback((_) => _run());
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: Expanded(child: Text(_text)),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => setState(() => _isTestPaused = !_isTestPaused),
            child: Icon(_isTestPaused ? Icons.play_arrow : Icons.pause),
          ),
        );
      }
    
      Future _run() async {
        int count = 0;
        Stream pauseStream = Stream.value(_isTestPaused).switchMap((_) => _isTestPaused ? Stream.periodic(const Duration(milliseconds: 100)) : Stream.value(false));
    
        _pauseSubscription = pauseStream.listen((_) {});
    
        for (int i = 0; i < 100; i++) {
          await Future.delayed(const Duration(seconds: 1));
          await pauseStream.first;
          setState(() => _text += '${count += 1}\n');
        }
    
        _pauseSubscription.cancel();
      }
    
      @override
      void dispose() {
        _pauseSubscription.cancel();
        super.dispose();
      }
    }
    
    

    En este código, creamos un Stream (pauseStream) que emite valores según el valor de _isTestPaused. Cuando _isTestPaused es true, el Stream emite valores cada 100 milisegundos, lo que hace que el await pauseStream.first; se pause hasta que _isTestPaused se establezca en false. Esto permite que la interfaz de usuario siga respondiendo mientras se pausa la ejecución.

    También es importante cancelar la suscripción al Stream en el método dispose para evitar fugas de memoria.

    Respondida el Dec 17, 2020 a las 16:24 - por Gemini

    Votos positivos: 0 | Votos negativos: 0