Flutter. Animations without StatefulWidget

Flutter. Animations without StatefulWidget

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

We will use the SimpleTickerProvider instead.

EUREKA OF THE DAY

If you are a member, please continue;otherwise, read the full story here.

The problem.

The AnimationControllerclass has a vsyncconstructor parameter, which should be an instance of TickerProvider.

Flutter has two [recommended] implementations of TickerProvider:

To obtain a TickerProvider, consider mixing in either TickerProviderStateMixin (which always works) or SingleTickerProviderStateMixin (which is more efficient when it works) to make a State subclass implement TickerProvider. That State can then be passed to lower-level widgets or other related objects. This ensures the resulting Tickers will only tick when that State's subtree is enabled, as defined by TickerMode.

So, the typical view, which has an animation, looks like this:


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,
),
);
}
}

So, where is the problem?

  1. StatefulWidgetsmix concerns;
  2. have complicated syntax;
  3. I don't like them.

I am a die-hard GetX user and don't need StatefulWidgets. 😋

(StatefulWidgets are fine for custom components with ephemeral states.)

Solution.

So, I ask myself a question: Is it hard to implement a TickerProvider?

And the answer is: No, it is easy[peasy].

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;
}
}

That's all.

Actually, the SimpleTickerProvideris like SingleTickerProviderMixin— it always uses the same instance of ticker.

Here is the mixin variant:


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;
}
}

Usage example with 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();
}
}

Note that we can use the mixin with vsync: this or the class with vsync: SimpleTickerProvider(). Both variants work.

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),
),
),
),
),
]),
),
);
}
}

Complete SoC accomplished: widgets with widgets, controllers with view models.

Result:

Usage example with 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();
}
}

ChangeNotifierdoesn't have an onInitmethod, so I used the constructor to initialize the animation.

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)
]),
),
);
}
}

Note that I am using the Disposerwidget to call the disposemethod of the ViewModel.

Final thoughts

I feel like I did something heretical here. 😎 If I share this article on FlutterDev, it will be downvoted to hell. Kids relax, I am just experimenting.

👉More stories about Flutter animations

Thank you for reading!

Report Page