Flutter. Анимации без StatefulWidget

Flutter. Анимации без StatefulWidget

FlutterPulse

Эта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Мы будем использовать SimpleTickerProvider вместо.

ЕВРЕКА ДНЯ

Если вы являетесь участником, пожалуйста, продолжайте;иначе, читайте полную историю здесь.

Проблема.

Класс AnimationController имеет параметр конструктора vsync, который должен быть экземпляром TickerProvider.

У Flutter есть две [рекомендуемые] реализации TickerProvider:

Чтобы получить TickerProvider, рассмотрите возможность смешивания либо TickerProviderStateMixin (который всегда работает), либо SingleTickerProviderStateMixin (который более эффективен, когда это работает), чтобы сделать подкласс State реализующий TickerProvider. Этот State затем может быть передан нижнеуровневым виджетам или другим связанным объектам. Это гарантирует, что resulting Tickers будут тикать только тогда, когда поддерево этого State включено, как определено TickerMode.

Таким образом, типичный вид, который имеет анимацию, выглядит так:

class AnimationPage extends StatefulWidget {
  const AnimationPage({super.key});

  @override
  State<AnimationPage> createState() => _AnimationPageState();
}

class _AnimationPageState extends State<AnimationPage>
    with TickerProviderStateMixin {

  late final AnimationController _controller;

  late final Animation<double> _sizeAnimation;

  double _size = 0.0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    )..addListener(() {
      setState(() {
        _size = _sizeAnimation.value;
      });
    });
    _sizeAnimation = _controller.drive(Tween(begin: 50.0, end: 150.0));
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Positioned(
            left: 20,
            top: 20,
            child: Container(width: _size, height: _size),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _controller.forward,
      ),
    );
  }
}

Так где же проблема?

  1. StatefulWidgetsсмешивают задачи;
  2. имеют сложный синтаксис;
  3. Я их не люблю.

Я ярый пользователь GetX и не нуждаюсь в StatefulWidgets. 😋

(StatefulWidgets подходят для пользовательских компонентов с временными состояниями.)

Решение.

Так, я задаю себе вопрос: Трудно ли реализовать TickerProvider?

И ответ: Нет, это легко[просто].

import 'package:flutter/scheduler.dart';

class SimpleTickerProvider implements TickerProvider {
  Ticker? _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    if (_ticker != null) {
      return _ticker!;
    }
    _ticker = Ticker(
      onTick,
    );
    return _ticker!;
  }

  void dispose() {
    _ticker = null;
  }
}

Вот и всё.

На самом деле, SimpleTickerProvider похож на SingleTickerProviderMixin — он всегда использует один и тот же экземпляр тикера.

Вот вариант с миксином:

import 'package:flutter/scheduler.dart';

mixin SimpleTickerProviderMixin implements TickerProvider {
  Ticker? _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    if (_ticker != null) {
      return _ticker!;
    }
    _ticker = Ticker(
      onTick,
    );
    return _ticker!;
  }

  void dispose() {
    _ticker = null;
  }
}

Пример использования с GetX

ViewModel:

class SizeController extends GetxController with SimpleTickerProviderMixin{
  late final AnimationController animationController;

  late final Animation<double> _sizeAnimation;
  double _size = 50.0;
  get size => _size;


  @override
  void onInit() {
    super.onInit();

    animationController = AnimationController(
     // vsync: SimpleTickerProvider(),
     vsync: this,
      duration: Duration(seconds: 1),
    )..addListener(() {
        _size = _sizeAnimation.value;
        update();
      });
    _sizeAnimation = animationController.drive(Tween(begin: 50.0, end: 150.0));
  }

  @override
  void onClose() {
     animationController.dispose();
     super.onClose();
  }
}

Обратите внимание, что мы можем использовать миксин с vsync: this или класс с vsync: SimpleTickerProvider(). Оба варианта работают.

View:

class SizeView extends GetView<SizeController> {
  const SizeView({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(

      ),
      body: Center(
        child: Stack(
          alignment: Alignment.center,
          children: [
          Positioned(
            top: 20,
            child: GetBuilder<SizeController>(
              builder: (controller) {
                return Container(
                  width: controller.size,
                  height: controller.size,
                  color: Colors.blue,
                );
              }
            ),
          ),
          Positioned(
            bottom: 200,
            child: Center(
              child: CupertinoButton.filled(
                onPressed: () {
                  controller.animationController.forward();
                },
                child: Text(
                  'Animate',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
          ),
        ]),
      ),
    );
  }
}

Полная реализация SoC: виджеты с виджетами, контроллеры с моделями представления.

Результат:

Пример использования с ChangeNotifier

ViewModel:

class SizeViewModel extends ChangeNotifier with SimpleTickerProviderMixin{
  late final AnimationController animationController;

  late final Animation<double> _sizeAnimation;
  double _size = 50.0;
  get size => _size;

  SizeViewModel() {
     animationController = AnimationController(
     // vsync: SimpleTickerProvider(),
     vsync: this,
      duration: Duration(seconds: 1),
    )..addListener(() {
        _size = _sizeAnimation.value;
        notifyListeners();
      });
    _sizeAnimation = animationController.drive(Tween(begin: 50.0, end: 150.0));
  }

  void dispose() {
    super.dispose();
    animationController.dispose();
  }
}

ChangeNotifierне имеет метода onInit, поэтому я использовал конструктор для инициализации анимации.

View:

class SizeView extends StatelessWidget{
  const SizeView({super.key});
  @override
  Widget build(BuildContext context) {
    var viewModel = SizeViewModel();
    return Scaffold(
      appBar: AppBar(

      ),
      body: Center(
        child: Stack(
          alignment: Alignment.center,
          children: [
          Positioned(
            top: 20,
          //  left: 20,
            child: ListenableBuilder(
              listenable: viewModel,
              builder: (context, child) {
                return Container(
                  width: viewModel.size,
                  height: viewModel.size,
                  color: Colors.blue,
                );
              }
            ),
          ),
          Positioned(
            bottom: 200,
           // left: 100,
            child: Center(
              child: CupertinoButton.filled(
                onPressed: () {
                  viewModel.animationController.forward();
                },
                child: Text(
                  'Animate',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
          ),
          Disposer(dispose: viewModel.dispose)
        ]),
      ),
    );
  }
}

Обратите внимание, что я использую виджет Disposer для вызова метода dispose ViewModel.

Заключительные мысли

Кажется, я сделал что-то еретическое здесь. 😎 Ребята, успокойтесь, я просто экспериментирую.

Спасибо за чтение!


Report Page