Flutter —Todo Animation — Development Journey

This implementation is based on dribbble shot by Gis1on. Thanks to Gis1on for the awesome design.

Note: This post is not traditional tutorial but I share the process I took and explain the code alone the way.

There are lots of developers who are doing Flutter Challenges. This actually tells a lot about Flutter as I have not seen a lot of similar implementations done in other mobile frameworks. This means that developers are happy challenging themselves with difficult design and they do not feel like it is tough assignment but want to push their skill and Flutter helps them do that.

I actually feel the same way and so I decided that the best way to learn Flutter is to do one of this challenges.

This post will describe the process I took to create a similar design from the one shown above. I am going to focus on the journey of how I got there as there are already many tutorials introducing Flutter to beginners. Let’s have a look at the final result.

My initial reaction with the design is that the transition is simple and beautiful and will be great to start learning animation for me. I will focus on the main aspect of the design which is the transition of the Floating Action button (the + icon on the bottom) to next screen button which becomes full width of the page.

Getting Started

Let’s start with the structure of the widgets in the Home Page. It is divided into three main segments

  • Header row
  • List of TODO items
  • Floating Action Button

This is pretty straight forward as you start small and build widget one by one and with the power of hot reload it’s easy to try different approaches before settling on the one you like.

Hero Widget

Hero Widget helps you in creating animation while the app transitions from one page to another. The requirement for this widget is that the Hero widget should be places in both pages and both the widget should have the same tag for flutter to identify them.

Hero widget did most of the heavy lifting for me animating the + icon. Honestly after seeing the design I thought all I need was to create a Hero widget from the Home screen to Add Todo screen but there were some obstacles along the way.

Hero Widget in Add Todo Page

Let’s have a look at the Hero Widget in the Add Todo Page and then we will circle back to Home page as it is little different there.

Hero(
tag: 'add_task',
child: ButtonTheme(
minWidth: double.infinity,
child: RaisedButton(
elevation: 10.0,
padding: const EdgeInsets.all(10.0),
color: Colors.blue,
onPressed: () {},
child: Icon(
Icons.add,
color: Colors.white,
),
),
),
)

This is the widget which was added to the second page which had the Text fields and + button at the bottom. Note that the Hero widget which will be placed in the Home Screen should also have the same tag add_task. ButtonTheme class is wrapped inside RaisedButton as I wanted it to stretch the entire width available.

Home Page Hero Widget

In the Home Page since we are trying to add Hero to Floating Action Button, Flutter already provides a property in FAB for that and its called as heroTag

floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
heroTag: 'add_task',
onPressed: () {
Navigator.pushNamed(context, '/addTodo');
},
),

This worked mostly well but there was a few issue with the implementation. Let’s have a look at the full build method for the add_todo.dart

TextField widget has a property called autofocus which when set to true will automatically focus on the text field when the page opens and then keyboard will open. I thought it will properly sync with the Hero Widget animation but as you can see from the below gif that did not happen

Fun fact here is that the animation is slowed down so as to view the source of the problem. Flutter provides a way to do this. Just add the below code in the build method of the widget.

import 'package:flutter/scheduler.dart' show timeDilation;
...
@override
Widget build(BuildContext context) {
timeDilation = 10.0 //Default is 1.0. So 10 times slower
...
}

Issues to fix

  1. Clicking the back button causes small issue as the keyboard is open and you can see yellow and black lines of flutter saying it has some overlapping. (This probably is very small and probably cannot be seen in production but it bugged me :P )
  2. Transition starts from 1/4 of the screen from top, moves up and fades in. This is the default of Material Routing transition when pushing a page.
  3. Keyboard is opened very quickly and the whole Hero animation is blocked below the keyboard. (Most important)

Issue 1 — Solution — Closing keyboard before closing page

First issue can be fixed by added a method to the page to check before it will pop out of stack and then closing the keyboard before closing the page.

Future<bool> _onWillPop() {
FocusScope.of(context).requestFocus(new FocusNode());
return Future.value(true);
}
...
@override
Widget build(BuildContext context) {
...
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
...
);

WillPopScope Widget has a property onWillPop which can be used to show a Popup to confirm before closing. I used it to close the Keyboard with the FocusScope.of(context).requestFocus(new FocusNode()); This code will make the focus go away from the current TextField, thus closing the keyboard.

Issue 2 — Solution — Custom Route Transition

After this I searched for a few ways to make the animation more visible and make it match the design spec, I wanted to add some curves to the animation in the Hero Widget but I could not figure out how to do that. Finally I decided to do create my own transition to new page to control the time and other aspects of the animation.

Since we have MaterialApp as the base widget when we push or pop a page, the default transitions for flutter comes from MaterialPageRoute<T> class. For this challenge I wanted the transition to happen from bottom to top and page should be sliding up. Lucky for me, iOS uses that exact transition when the page is full page dialog page. We can extend from CupertinoPageRoute<T> to take advantage of that transition

class MyCustomRoute<T> extends CupertinoPageRoute<T> {
MyCustomRoute({WidgetBuilder builder, RouteSettings settings}): super(builder: builder, settings: settings);
@override
bool get fullscreenDialog => true;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
}

I have overridden a couple of properties

  • fullscreenDialog — Set it true to make sure it has bottom to top transition
  • transitionDuration — This controls the time taken for the whole animation
onPressed: () {
Navigator.push(
context,
new MyCustomRoute(builder: (context) => new AddTodoScreen()),
);
},

OnPressed on the button get modified like this so as to take advantage of the new Custom Transition

Issue 3 — Solution-Opening keyboard after transition

I solved the third issue by making sure that the keyboard is only opened when the transition of the page is complete. For this we have to have control on the focus event on the TextField.

final FocusNode textFieldFocusNode = FocusNode();
....
TextField(
focusNode: textFieldFocusNode,
...
)

Creating a FocusNode field in the class and we can assign it to the TextField. So now we can decide when we want the focus to be changed to this TextField.

@override
void didChangeDependencies() {
super.didChangeDependencies();
_focusOnTextField();
}
...
void _focusOnTextField() async{
await Future.delayed(Duration(milliseconds: 300));
FocusScope.of(context).requestFocus(textFieldFocusNode);
}
...

Finally, we need to call the method which will set focus on the TextField after the transition is done. We have set the transition timing in the Custom Route and so we know how much time it would take for the transition.

We cannot call this method in the initState method as the page is not ready at the time. So we can override another method called didChangeDependencies

Docs — didChangeDependencies
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an InheritedWidget that later changed, the framework would call this method to notify this object about the change.
This method is also called immediately after initState. It is safe to call BuildContext.inheritFromWidgetOfExactType from this method.

Since FocusScope.of(context) is accessing InheritedWidget, it is safe to call it in the didChangeDependencies method instead of initState.

So now all three bugs are crushed. The transition of the view is same as what is show at the start.

Conclusion

That brings us to the current state of the app which has thought me more about Flutter than before. I know that app does not fully have the same slick animation as the design but I got it as close. If there is a better way to do this effect, I would love to know about that in the comments.

Comments about the content and style of this post is also appreciated. Please share the post if your like it.


The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.