Как приостановить и возобновить StreamProvider в Flutter

#flutter #dart #flutter-provider

Вопрос:

Я экспериментирую с обновлением forex в реальном времени с помощью StreamProvider . Демонстрационная версия будет автоматически обновлять обменный курс, периодически получая последние данные из внешнего API. (в этом примере каждые 60 секунд)

Ниже приведена схема реализации.

Диаграмма

 API call (Future event) --> Put data in stream
      ^                             |
      |                             V              
Wait for 60 seconds <--  StreamProvider listens for 
                        new event and rebuild widget

 

Проблема

Поток продолжается даже при переходе к главному виду.

Если мы используем StreamBuilder , мы, возможно, сможем вызвать listen() метод, который вернется StreamSubscription . Затем либо cancel() , pause() , либо resume() метод может быть вызван по требованию. Интересно, есть ли подобный метод pause и resume при использовании StreamProvider ?

Ожидаемый

pause при выходе из представления панели мониторинга и resume при возвращении в представление панели мониторинга.

Коды

Модель

 
class Currency {
  String? base;
  String? quote;
  double? rate;

  // constructor, factory constructor, etc.
  // ...
}

 

Контроллер

 class CurrencyService {
  Currency? _currency;

  Stream<Currency?> get currencyStream async* {
    yield* Stream.periodic(Duration(seconds: 60), (_) {
      return getCurrencyData();
    }).asyncMap((event) async => await event);
  }

  Future<Currency?> getCurrencyData() async {
    try {
      // Perform API call and 
      // update Currency object
      // ...
      
    } catch (e) {
      print('Error: $e');
    }
    return _currency;
  }
}
 

View

 void main() async {
  runApp(
    MultiProvider(
      providers: [
        // some providers,
        // another one,
        // ...
        StreamProvider<Currency?>(
          create: (_) => CurrencyService().currencyStream,
          initialData: await CurrencyService().getCurrencyData(),
        ),
      ],
      child: TestApp(),
    ),
  );
}

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Live Update Demo',
      initialRoute: '/',
      routes: routes,
    );
  }
}
 

Main view (page 1)

 class MainView extends StatefulWidget {
  const MainView({Key? key}) : super(key: key);

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

class _MainViewState extends State<MainView> {

  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          // ...
          ElevatedButton(
            onPressed: () {
              Navigator.pushNamed(context, '/dashboard');
            },
            child: Text('Dashboard')),
        ],
      ),
    );
  }
}
 

Вид панели мониторинга (страница 2)

 class DashboardView extends StatelessWidget {
  const DashboardView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Consumer<Currency?>(
      builder: (context, currency, child) {
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(
                child: Text('${currency?.base ?? ''}${currency?.quote ?? ''}'),
              ),
              Container(
                child: Text('${currency?.rate ?? ''}'),
              ),
            ],
          ),
        );
      },
    ));
  }
}
 

Спасибо.

Комментарии:

1. A Stream.periodic нельзя останавливать и возобновлять. Он будет работать вечно, пока не закроется, и в этот момент он будет навсегда закрыт. Вместо этого используйте a Timer для запуска периодического события и передачи данных в обычное StreamController состояние, которое могут прослушивать ваши виджеты.

2. @Abion47 спасибо за предложение. Я пересмотрел некоторые части, и это действительно работает. Я добавлю эту редакцию в раздел ответов ниже. Тем не менее мне любопытно и интересно узнать о подобной реализации с использованием StreamProvider

3. Вы все еще можете использовать StreamProvider . Вместо использования конструктора по умолчанию используйте StreamProvider.value для предоставления внешнего источника потока.

4. @Abion47 Я заменяю на StreamBuilder , StreamProvider.value и это, наконец, работает! Требуется некоторое время, чтобы понять, как его использовать Timer.periodic . Еще раз спасибо!

Ответ №1:

Приостановка и возобновление StreamProvider Stream.periodic работы кажутся невозможными. Вместо этого реализация все еще может быть достигнута с помощью Timer.periodic и StreamController , как предлагает @Abion47

Мы можем имитировать паузу и возобновление, контролируя, когда начинать и прекращать добавление новых данных в поток. Один из способов-запустить Timer.periodic при переходе к представлению панели мониторинга (после нажатия кнопки) и cancel таймер при возвращении в основной вид (откроется представление панели мониторинга).

 ElevatedButton(
  onPressed: () {
    // start timer
    // ...
    Navigator.pushNamed(...).then((_) { 
      // stop timer
      // this section is triggered when returning from dashboard to main view
    });
}
 

Пересмотренные кодексы

 // Controller

class CurrencyService {
  Currency? _currency;
  Timer? _pollingTimer;
  StreamController<Currency?> _currencyController = StreamController.broadcast();

  Future<void> addCurrencyData() async {
    await getCurrencyData()
        .then((currency) => _currencyController.add(currency));
  }

  void closeStream() {
    _currencyController.close();
  }

  void startPolling() {
    addCurrencyData();
    _pollingTimer = Timer.periodic(Duration(seconds: 60), (_) => addCurrencyData());
  }

  void stopPolling() {
    _pollingTimer?.cancel();
  }

  Stream<Currency?> get currencyStream => _currencyController.stream;

  Future<Currency?> getCurrencyData() async {
    try {
      // Perform API call and 
      // update Currency object
      // ...
      
    } catch (e) {
      print('Error: $e');
    }
    return _currency;
  }
}

// Main

void main() async {
  runApp(
    MultiProvider(
      providers: [
        // some providers,
        // another one,
        // ...
        Provider(create: (_) => CurrencyService()),
      ],
      child: TestApp(),
    ),
  );
}

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Live Update Demo',
      initialRoute: '/',
      routes: routes,
    );
  }
}

// Main view (page 1)

class MainView extends StatelessWidget {
  const MainView({Key? key}) : super(key: key);

  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          // ...
          ElevatedButton(
            onPressed: () {
              Provider.of<CurrencyService>(context, listen: false)
                .startPolling();
              Navigator.pushNamed(
                context, 
                '/dashboard',
              ).then((_) => Provider.of<CurrencyService>(context, listen: false).stopPolling());
            },
            child: Text('Dashboard')),
        ],
      ),
    );
  }
}

// Dashboard view (page 2)

class DashboardView extends StatelessWidget {
  const DashboardView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final currencyService = Provider.of<CurrencyService>(context);
    return Scaffold(
      body: StreamProvider<Currency?>.value(
        initialData: null,
        value: currencyService.currencyStream,
        child: CurrencyRate(),
      ),
    );
  }
}


class CurrencyRate extends StatelessWidget {
  const CurrencyRate({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final currency = context.watch<Currency?>();
    return Center(
      child: currency == null
        ? CircularProgressIndicator()
        : Column(
            mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  child: Text('${currency?.base ?? ''}${currency?.quote ?? ''}'),
                ),
                Container(
                  child: Text('${currency?.rate ?? ''}'),
                ),
              ],
            ),
         );   
      } 
}