Flutter — Interactive Diary — A developer diary #11
Tags: flutter
Published: 2022-09-11 10:53:33

## Have fun with Animations

Let’s put the Dev hat on, and enjoy the fun of code today.

As I’m switching back to Dev role, I look for the most interesting work I always like: Animation. And there’s 1 story fits nicely in my interest: [Core UI — Date text field with error integration.](https://github.com/suesitran/interactive_diary/issues/33)

Here is the design I came up with.

![Text field without error
](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1t3bvx9bpq4ddzoxzdps.png)

And it can be transform into below UI, when error is shown

![Text field with error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nppa7yn1pu6p9u5rvapn.png)

I also came up with a nice description of the animation I wish to have

> Animation
> When switching from normal state to error state, the error banner should slide down from behind the Date banner
> When switching from error state to banner state, the error banner should slide up to hide behind the Date banner
> Animation duration is 300ms

Now, let’s start coding.

## Code Plan

Code plan is actually to analyse the problem at hand, and try to solve it with a plan, before I write any code at all.

For simple problem, I usually just think of what to do, then remember the step, and start writing the code. But for this problem, there are actually 2 different hard-to-remember solutions I can try, so I’m gonna write them down here, in case I may forget them.

The first step is to create the UI. Looking at the 2 UI (with and without error), There are 2 ways to create this UI

- Create a `Stack`, with Label on top, and Error underneath
- Create a `Column`, and show/hide error as needed

Now, let’s analyse each solution

**Solution 1 — Stack**

So I’ll need a `Stack`, a large text for Label, and smaller text for error

Stack: need error background colour, so that when the error text is showed, this background colour will be showed.

Label: need container to wrap around it because I need the decoration, with border radius, and background colour

Error: Just text, white colour, with some padding
Now, let’s think of animation.

> When switching from normal state to error state, the error banner should slide down from behind the Date banner.

This suggest I can use `SlideTransition`. But if I use `SlideTransition` on the error text, only the error text will slide down, without the red background colour. So no, not `SlideTransition`.

Next implementation I can think of is to create Error with its own container, with decoration and background colour to be error colour, then use `AnimatedBuilder` to update the height of this container.

But to what height should I change this container’s height to?

I know the size of the Error text, I know the padding value I wanna give to it. So maybe, I can calculate this height.

Okie, this sounds fun, although it does not sound like the best solution. Why? Because

- with Accessibility large text, there’s a good chance that the calculation will fail

- and each time to show/hide error, I’ll have to do a calculation again.

This sounds silly, but actually it will work. Actually I coded this as well in [this PR](https://github.com/suesitran/interactive_diary/pull/40), and tried to stop here, because this works.

But after thinking again and again, this is not best approach yet. So, let’s look at the 2nd solution.

**Solution 2 — Column**

This solution is straight-forward. First, I create a `Column` with 2 children: the `Text` for Label, and the `Text` for error.

I’ll need some decoration for Label, border radius, background colour, text size, text colour.

Then I’ll look at the error. If I create decoration for the Error, when the error is showed, it will totally be separated from the Label, and I will not have the effect that the Error is extended from the Label above.

To solve this, I need a container to wrap outside of the `Column`, and apply decoration for the Error on this `Container`.

When there’s no Error, this decoration is totally hidden behind the Label decoration, but when there is error, the lower part will be visible as if it’s the background of the error.

But wait, there’s shadow in the UI. In this case, I can wrap them in a `Card`, and use its elevation value to show shadow, and I can also decorate the `Card` by updating its shape.

Now, the animation part. The simplest animation is to change the height of this Error text, and create the illusion that the text slide down from underneath the Label.

The only thing I’m worried here is that the text in Error may be distorted during animation.

If I use the SizeTransition widget, I can change the value of axisAlignment, so maybe this can help saving the text from being distorted due to layout change.

That sounds like a good plan. It’s now time to code.

## Write the code

Here is the [link to the complete code of solution 2](https://github.com/suesitran/interactive_diary/pull/43). And below is detail how the Code plan matches the code.

First step, create a `StatelessWidget` to host the custom widget I want to create.

Then, in the build method of this `StatelessWidget`, I create a `Column` widget, with 2 children: 1 `Text` for label, and 1 `Text` for error.

```
Column(
  children: [
    Text('label'),
    Text('error'),
  ],
)
```
But I need decoration for the label text, so I’ll separate it out to another method, to make it easy to implement and edit later. And I’ll do the same for error text as well, because it’ll need animation. The code now becomes

```
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    _labelWidget(),
    _errorWidget(),
  ],
)
```

At this time, the 2 methods `_labelWidget()` and `_errorWidget()` are simply returning Text widget.

Now, let’s work on this line

> I’ll need some decoration for Label, border radius, background colour, text size, text colour.

This implementation will go into `_labelWidget()` method, and I’ll need a `Container` widget for this. And also need to update the `textStyle` of this label to match the design.

```
Widget _labelWidget() => Container(
  alignment: Alignment.center,
  decoration: BoxDecoration(
      color: Theme.of(context).colorScheme.onBackground,
      borderRadius: const BorderRadius.all(
          Radius.circular(NartusDimens.padding24))),
  padding: const EdgeInsets.symmetric(
      horizontal: NartusDimens.padding16,
      vertical: NartusDimens.padding14),
  child:
  Text('Label', style: Theme.of(context).textTheme.headline6),
);
```

Then I also need to update the padding for my error text.

```
Widget _errorWidget() => Padding(
  padding: const EdgeInsets.symmetric(
      horizontal: NartusDimens.padding16,
      vertical: NartusDimens.padding4),
  child: Text(
    'error',
    style: Theme.of(context)
        .textTheme
        .subtitle1
        ?.copyWith(color: Theme.of(context).colorScheme.onError),
    textAlign: TextAlign.center,
  ),
);
```

Right now, the UI of this widget component looks like this

![First draft of text_and_error_label widget](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqz82tzik5r9a4d8n5mf.png)

I still need decoration for the background of error text. So I’ll wrap the `Column` inside a `Card` widget, then do decoration on the `Card` itself.

```
Card(
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(NartusDimens.padding24)),
  ),
  color: Theme.of(context).colorScheme.error,
  elevation: NartusDimens.padding4,
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
      _labelWidget(),
      _errorWidget(),
    ],
  ),
)
```

![text_and_error_label after Card decoration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6t16avvcid5i4o7y88g.png)

Now it looks matching the designed UI. It’s time to implement animation.

In order to add animation to this widget, I’ll need to use `SingleTickerProviderStateMixin`, and with this mixin, I’ll need to convert my `StatelessWidget` to be a `StatefulWidget`, then implement the `State` of my `StatefulWidget` with `SingleTickerProviderStateMixin`

```
class _TextAndErrorLabelState extends State<TextAndErrorLabel>
 with SingleTickerProviderStateMixin {}
```

Actually I used Android Studio to do the conversion, so it’s like there’s no work on me at all.

![Android Studio can easily convert StatelessWidget to StatefulWidget](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p3uu9hsyjoiivi0kpcb0.png)

I’m going to use `SizeTransition` widget, so I’ll need an `AnimationController` to control my animation, which will have the duration of 300ms.

```
late final AnimationController _controller = AnimationController(
    vsync: this, duration: const Duration(milliseconds: 300));
```

and an Animation with double for the actual animation.

```
late final Animation<double> _sizeAnimation =
    CurvedAnimation(curve: Curves.fastOutSlowIn, parent: _controller);
```

The controller will need a value, so I’ll define this value in `initState()`

```
@override
void initState() {
  super.initState();

  _controller.value = 1;
}
```

And of course, I also need to dispose the controller when I no longer need it.

```
@override
void dispose() {
  super.dispose();

  _controller.dispose();
}
```

All setup is done, now it's the time to wrap the error inside a `SizeTransition`.

The `SizeTransition` widget will take full space of parent’s width, so my error text will no longer be center align. Therefore I need to change to use `Container` to wrap it, in order to have both padding and alignment in 1 container.

I also change the `axisAlignment` value to 1 (default is 0) because I want the `Container` to update the size from the bottom of its height, not the center or top.

```
Widget _errorWidget() => SizeTransition(
      sizeFactor: _sizeAnimation,
      axisAlignment: 1,
      child: Container(
        alignment: Alignment.bottomCenter,
        padding: const EdgeInsets.symmetric(
            horizontal: NartusDimens.padding16,
            vertical: NartusDimens.padding4),
        child: Text(
          'error',
          style: Theme.of(context)
              .textTheme
              .subtitle1
              ?.copyWith(color: Theme.of(context).colorScheme.onError),
          textAlign: TextAlign.center,
        ),
      ),
    );
```

And I think this is done.

But wait. Is it done? There’s no animation although I already added `SizeTransition` in my error text.

The reason is because I still need to use the `AnimationController` to start my animation. To do this, simply run `_controller.forward();` when it’s needed. In my case, because I’m going to use this widget in a bloc, so I’ll need it to run animation immediately as it’s showed, therefore this code will be in my `build()` method.

And that’s it. The animation works!

![Text and Error label demo with animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xtgy6rzde8xqru5duimu.png)

The full source code for this widget with animation can be found [in this link](https://github.com/suesitran/interactive_diary/blob/dev/packages/nartus_ui_package/lib/widgets/text_error_label/text_and_error_label.dart).

That’s all for today. If you like my post, please follow me for more tips.