When faced with the task of coming up with a re-usable "Animated Gradient" widget, I faced one problem, a gradient isn't actually a widget - I can't go and make an AnimatedWidget that I can use inside a BoxDecoration in my UI somewhere.

What I wanted was an actual gradient that simply took care of its own state management... not a widget, stateful or otherwise.

Enter hooks

Since discovering hooks for flutter, I've become a huge fan, I'm making heavy use of hooks in my architecture, as shown in this post, and I find them particularly useful for use-cases such the one I'm about to outline below.

A hook is a new kind of object that encapsulates behaviour, keeping the state management out of your widget hierarchy. Hooks must be used within the build method of a Hook Widget, or within another hook. Hooks; combined with HookWidget, are a drop-in replacement for StatefulWidget and either enhance or replace other forms of state management you might be using.

For animation, hooks are particularly useful because they replace a huge swath of boilerplate and the need to create multiple classes - with just a couple of lines of code.

For this example we are going to make our own custom hook that itself contains a bunch of other hooks. We will be able to use our hook anywhere we might use a gradient.

Tutorial time!

Full source code is available at: https://github.com/paddo/flutter_animated_gradient

Fire up a terminal.

flutter create animated_gradient code animated_gradient

Open up pubspec.yaml, and add the flutter_hooks dependency, as below:

name: animated_gradient description: A new Flutter project. version: 1.0.0+1 environment: sdk: ">=2.1.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_hooks: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true

Note, we are building against flutter - stable channel - v1.5.4-hotfix.2. If you are building against a different version of flutter, you may need to use a different version of flutter_hooks, and target a different SDK version.

Let's make a custom tween, and a custom hook

Since we are animating a gradient, we'll need to make a tween for it.

Custom hooks are pretty easy to make. You simply need to create a Hook class and a corresponding HookState. Our hook, when used, should return a gradient, so it will be a Hook<Gradient>.

Create a new file: lib/animated_gradient.dart

import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; Gradient useAnimatedGradient({ Duration duration = const Duration(seconds: 5), List<Gradient> gradients, Curve curve = Curves.linear, }) { return Hook.use(_AnimatedGradientHook( duration: duration, gradients: gradients, curve: curve, )); } class GradientTween extends Tween<Gradient> { GradientTween({ Gradient begin, Gradient end, }) : super(begin: begin, end: end); @override Gradient lerp(double t) => Gradient.lerp(begin, end, t); } class _AnimatedGradientHook extends Hook<Gradient> { final List<Gradient> gradients; final Duration duration; final Curve curve; _AnimatedGradientHook({this.duration, this.gradients, this.curve}); @override HookState<Gradient, Hook<Gradient>> createState() => _AnimatedGradientHookState(); } class _AnimatedGradientHookState extends HookState<Gradient, _AnimatedGradientHook> { @override Gradient build(BuildContext context) { final controller = useAnimationController(duration: hook.duration); final index = useValueNotifier(0); useEffect(() { controller.repeat(); final listener = () { final newIndex = (controller.value * hook.gradients.length).floor() % hook.gradients.length; if (newIndex != index.value) index.value = newIndex; }; controller.addListener(listener); return () => controller.removeListener(listener); }, [hook.gradients, hook.duration, hook.curve]); return useAnimation(GradientTween( begin: hook.gradients[index.value], end: hook.gradients[(index.value + 1) % hook.gradients.length]) .animate(CurvedAnimation( curve: Interval( index.value / hook.gradients.length, (index.value + 1) / hook.gradients.length, curve: hook.curve, ), parent: controller))); } }

Explainer time:

Put simply, we have an animation on repeat, and based on where we are in that animation, we interpolate between 2 gradients, all of these interpolations join together (and cycle back to the start ), to form one smooth sequence.

Our hook implementation lives inside a private class, and has to be used via the "useAnimatedGradient" function, as is the convention with the built-in hooks. These private classes should look pretty similar to StatefulWidget boilerplate.

Our initialisation and tear-down code is in the useEffect hook. Here we attach a listener, which is responsible for selecting the pair of gradients that we are presently animating between, and the corresponding listener tear-down code. See useEffect docs.

The useAnimationController hook takes care of all the boilerplate required for animations - the Ticker Mix-ins, initialisation code, disposal, etc.

Since we do need some state information that survives between rebuilds (namely, the present position in the array of gradients), we use a useValueNotifier hook. We could've used useState (which forces a rebuild), but in our use-case, we don't have to.

Using the hook

If you want to go and use it in your own project, you can use this hook with a list of gradients to cycle between. Use it anywhere you might use a gradient. Good luck!

For the sake of completeness though, and to bring this short sample to a conclusion, I'm going to make a widget to house our animation, so that we can encapsulate the state (and rebuilds) into as small a surface area as possible within the widget hierarchy.

Make a new file: lib/animated_gradient_box

import 'package:animated_gradient/animated_gradient.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; class AnimatedGradientBox extends HookWidget { final List<Gradient> gradients; final Curve curve; AnimatedGradientBox(this.gradients, [this.curve = Curves.linear]); @override Widget build(BuildContext context) { return Container(decoration: BoxDecoration(gradient: useAnimatedGradient(gradients: gradients, curve: curve))); } }

Now, layout a bunch of these boxes, and let the psychedelic adventures commence.

Make lib/main.dart look like this:

import 'package:animated_gradient/animated_gradient_box.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final List<Gradient> _linearGradients = [ LinearGradient( colors: [Colors.red, Colors.purple, Colors.blue, Colors.green, Colors.yellow], begin: Alignment.centerLeft, end: Alignment.centerRight), LinearGradient( colors: [Colors.red, Colors.purple, Colors.blue, Colors.green, Colors.yellow], begin: Alignment.topCenter, end: Alignment.bottomCenter), LinearGradient( colors: [Colors.red, Colors.purple, Colors.blue, Colors.green, Colors.yellow], begin: Alignment.centerRight, end: Alignment.centerLeft), LinearGradient( colors: [Colors.red, Colors.purple, Colors.blue, Colors.green, Colors.yellow], begin: Alignment.bottomCenter, end: Alignment.topCenter), ]; final List<Gradient> _sweepGradients = [ SweepGradient(colors: [Colors.red, Colors.yellow, Colors.green, Colors.red]), SweepGradient(colors: [Colors.lightBlue, Colors.purple, Colors.pink, Colors.lightBlue]), ]; final List<Gradient> _radialGradients = [ RadialGradient(colors: [Colors.red, Colors.blue], radius: 1.0, center: Alignment.topCenter), RadialGradient(colors: [Colors.lightBlue, Colors.purple, Colors.pink], radius: 2.0, center: Alignment.bottomCenter), ]; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Expanded(child: AnimatedGradientBox(_linearGradients)), Expanded(child: AnimatedGradientBox(_sweepGradients)), Expanded(child: AnimatedGradientBox(_radialGradients, Curves.easeInOut)), Expanded( child: AnimatedGradientBox( []..addAll(_linearGradients)..addAll(_radialGradients)..addAll(_sweepGradients))), ], ), ), ); } }

Behold the amazing result!

May induce seizures

Source code is available at https://github.com/paddo/flutter_animated_gradient

I'm also working on a flutter architecture series. See Part 1.