Pull to refresh

Тик-так: Timer vs Ticker для анимаций

Level of difficultyEasy
Reading time4 min
Views890

Всем привет! Это статья для тех, кто увлекается 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.

Tags:
Hubs:
Total votes 2: ↑2 and ↓0+4
Comments0

Articles