#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
нельзя останавливать и возобновлять. Он будет работать вечно, пока не закроется, и в этот момент он будет навсегда закрыт. Вместо этого используйте aTimer
для запуска периодического события и передачи данных в обычное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 ?? ''}'),
),
],
),
);
}
}