Всем привет! Это статья для тех, кто увлекается Flutter-разработкой. А я Федор — разработчик Mad Brains. Поговорим о Timer и Ticker?
Итак, представим, что нам нужно построить экран, в котором будет отображаться текущее Unix-время в миллисекундах. Давайте сначала сделаем верстку без анимации.
В исходном коде нет ничего необычного — пара виджетов и ValueNotifier _msSinceEpoch для Unix-времени👇
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final ValueNotifier<int> _msSinceEpoch;
@override
void initState() {
super.initState();
_msSinceEpoch = ValueNotifier(DateTime.now().millisecondsSinceEpoch);
}
@override
void dispose() {
_msSinceEpoch.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Home page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_TimerCard(
child: ValueListenableBuilder<int>(
valueListenable: _msSinceEpoch,
builder: (BuildContext context, int value, ___) => Text(
value.toString(),
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
Text(
'milliseconds since epoch',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
);
}
}
class _TimerCard extends StatelessWidget {
const _TimerCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
);
}
}
Используем Timer
Теперь переходим к главному вопросу — как обновлять текущее время в _msSinceEpoch? Первое, что приходит на ум — это использовать Timer.periodic.
В нем мы будем вызывать callback на обновление значения.
class _HomePageState extends State<HomePage> {
late final ValueNotifier<int> _msSinceEpoch;
late final Timer _timer;
@override
void initState() {
super.initState();
_msSinceEpoch = ValueNotifier(DateTime.now().millisecondsSinceEpoch);
_timer = Timer.periodic(
const Duration(milliseconds: 16),
(timer) {
_msSinceEpoch.value = DateTime.now().millisecondsSinceEpoch;
},
);
}
@override
void dispose() {
_timer.cancel();
_msSinceEpoch.dispose();
super.dispose();
}
// ...
}
Я залочил обновление таймера на 16 мс ради обновления виджета 60 раз в секунду. Всё хорошо работает, даже видно изменение времени, но у этого решения есть свои минусы.
Чем плох Timer?
⛔️ Нет удобного решения для работы в 60/120 FPS в зависимости от частоты экрана телефона
⛔️ Зависимость от времени, а не от построения кадра
⛔️ При скрытии виджета с экрана таймер продолжить работать и вызывать callback
Используем Ticker
К счастью, Flutter содержит встроенное решение, которое одновременно и покрывает возможности таймера, и лишено его минусов.
Встречайте, Ticker! Я думаю, вам уже не раз приходилось работать с ним, но не напрямую, а через AnimationController, который создаёт Ticker внутри себя.
Чтобы работать с Ticker'ом, нужно добавить миксин SingleTickerProviderStateMixin (или TickerProviderStateMixin) к стейту виджета. Так у нас появляется доступ к методу createTicker внутри этого стейта.
Взаимодействовать с ним также просто, как и с таймером.
Как работает Ticker
Ticker требует SchedulerBinding зарегистрировать callback
SchedulerBinding сообщает Flutter Engine, что надо разбудить Ticker, когда появится новый callback
Когда Flutter Engine готов, он вызывает SchedulerBinding через запрос onBeginFrame
SchedulerBinding обращается к списку обратных вызовов запланированный Ticker'ами и выполняет каждый из них
Если анимация завершена, то Ticker отключается, иначе Ticker запрашивает SchedulerBinding для планирования нового callback
Заменяем Timer на Ticker
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
late final ValueNotifier<int> _msSinceEpoch;
late final Ticker _ticker;
@override
void initState() {
super.initState();
_msSinceEpoch = ValueNotifier(DateTime.now().millisecondsSinceEpoch);
_ticker = createTicker((Duration elapsed) {
_msSinceEpoch.value = DateTime.now().millisecondsSinceEpoch;
});
// Главное не забыть включить
_ticker.start();
}
@override
void dispose() {
_ticker.dispose();
_msSinceEpoch.dispose();
super.dispose();
}
// ...
}
Чем хорош Ticker?
Автоматически вызывает callback в зависимости от частоты экрана телефона
Зависит от вызова построения кадра SchedulerBinding.onBeginFrame
Не вызывает callback, если стейт не в дереве
Вывод
Старайтесь по возможности сократить использование Timer для анимаций. И почаще используйте Ticker.