DEV Community

Guilherme Cabral for Runtime Revolution

Posted on • Updated on

Building a cross-platform todo mobile app using Flutter: Pt. 2

Photo by Randall Ruiz on Unsplash

Photo by Randall Ruiz on Unsplash

In the first part of this series (which you should totally take a look at), we set up our workspace to build apps with Flutter. In this part, we will take a more in-depth tour on how we can build stuff with it, and even create ourselves a pretty basic to-do app.

As you may remember, we were looking at the entry-point of every Dart application, the main.dart file. This file builds a simple counter app with a button that increments a variable, which then gets printed on the screen. That’s pretty useless, isn’t it? Let’s build something better. Let’s build a to-do app.

Before starting, I think we should delete the code from the example app I mentioned above and start from scratch. To do that, make your main.dart file look like this one:

There are some things you should focus on:

  • void main() => runApp(new MyApp()); is the entry-point of the application

  • the MyApp class, that holds the root of the widget tree

  • MaterialApp() is a widget provided by the library imported in the first line of the file. It builds a Material Design app.

  • the HomePage class, a widget that has a State instance of the class _HomePageState, which will return more than a Container() soon enough.

There are lots of details about this, and we’ll cover them as we go.

To-do lists have to-do items, right? So we need to describe Items using a simple class. To do so, we’ll make a folder called models and inside we’ll create a file called item.dart that looks like this:

Now that we have the model, let’s work on presenting our tasks by using some widgets! By importing Flutter’s Material library, we have access to a bunch of different pre-designed widgets to build our interface.

We need some to-do items to display. First we need to import the Item model we created earlier:

import 'package:todo/models/item.dart';

And then we place our tasks inside our _HomePageState class, like this:

class _HomePageState extends State<HomePage> {

  List<Item> items = [
    Item('Take a shower'),
    Item('Go shopping'),
    Item('Feed the cat'),
  ];

  ...

Now that we have items to show, let’s grow our widget tree to display them by changing our build method to this:

@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text('Awesome Todo'),
    ),
    body: new Container(
      child: new ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(
              '${items[index].title}',
            ),
            trailing: new IconButton(
              icon: new Icon(Icons.delete),
              onPressed: null,
            ),
          );
        },
      ),
    ),
  );
}

If you’re using Visual Studio Code or IntelliJ IDEA, and have already installed the respective Flutter plugin, you can start the app from the editor by pressing F5 on VS Code or clicking the green play button on IntelliJ IDEA (you also have hot-reload-on-save awesomeness). Run the app, and your device or simulator should look like the image below.

Ew, who can’t remember to take a shower?

Ew, who can’t remember to take a shower?

Let’s go over the widgets we used. The Scaffold is the basic structure for our app. It holds, among other things, the AppBar which we see on the top, and a Container as our body. It could also hold a drawer menu or other things. The Container on the body is the parent of a ListView.builder(). We could have just used ListView(children: items) and everything would be fine. I chose to go with ListView.builder() because this way it automagically renders its children (ListTile in this case) as they are scrolled through, similar to an infinite scroll.

We’ll need to add to-do items to our awesome list. Let’s start by adding a FloatingActionButton to our app! We’ll add it to our Scaffold, and this should be the result:

@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text('Awesome Todo'),
    ),
    body: new Container(
      child: new ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(
              '${items[index].title}',
            ),
            trailing: new IconButton(
              icon: new Icon(Icons.delete),
              onPressed: null,
            ),
          );
        },
      ),
    ),
    floatingActionButton: new FloatingActionButton(
      onPressed: null,
      tooltip: 'Increment',
      child: new Icon(Icons.add),
    ),
  );
}

Look at it, so pretty.

Look at it, so pretty.

And now we have a great Floating Action Button on our app!

It is pretty, but dumb: right now, the button does nothing. Neither do the trash cans on the items. Let’s create the methods that will handle those interactions and update our FloatingActionButton and ListTile widgets.

_onAddItemPressed() {

}

_onDeleteItem(item) {

}

floatingActionButton: new FloatingActionButton(
  onPressed: () {
    _onAddItemPressed();
  },
  tooltip: 'Increment',
  child: new Icon(Icons.add),
),

...

trailing: new IconButton(
  icon: new Icon(Icons.delete),
  onPressed: () {
    _onDeleteItem(index);
  },
),

To delete the Item from the list, we’ll just have to add this to the method we created above:

_onDeleteItem(item) {
  items.removeAt(item);
  setState(() {});
}

To add new items, I wanted to make things prettier and a little more fun. We’ll have a drawer coming from the bottom of the screen. That drawer will have a TextField that the user can fill and submit a task to the list. To do this, we are going to use a BottomSheet, more specifially, the Persistant kind. PersistantBottomSheets are context-aware, which means that opening them counts as navigating to another route of our app and that can be closed by pressing back in the app or in the device¹ (if running on an Android phone). To do so, we’ll need to set a key on our Scaffold widget. First, we’ll create it:

final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

Set it on our Scaffold widget:

@override
Widget build(BuildContext context) {
  return new Scaffold(
    key: _scaffoldKey,
    ...

and now we can use it to open the PersistantBottomSheet

_onAddItemPressed() {
  _scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) {
    return new Container(
      decoration:
          new BoxDecoration(color: Colors.blueGrey),
      child: new Padding(
        padding: const EdgeInsets.fromLTRB(32.0, 50.0, 32.0, 32.0),
      ),
    );
  });
}

This will show a sliding drawer coming from the bottom, with a Container, that has styles from a BoxDecoration widget, and has a Padding widget as a child (remember when I said that everything is a widget?).

If you go to your device and hit the Floating Button, you’ll see it rise to a small grey box to the left. That’s our Padding widget, being empty. Nothing to worry about, as we’re going to insert our TextField there. To achieve this, we’ll need a TextEditingController to handle our TextField and we’ll set it right after our _scaffoldKey :

final TextEditingController _textEditingController =
new TextEditingController();

and give our Padding widget a child.

child: new Padding(
  padding: const EdgeInsets.fromLTRB(32.0, 50.0, 32.0, 32.0),
  child: new TextField(
    controller: _textEditingController,
    decoration: InputDecoration(
      hintText: 'Please enter a task',
    ),
    onSubmitted: _onSubmit,
  ),
),

Right now it won’t build because we are using the _onSubmit function on the TextField and we haven’t created it yet. It’s pretty similar to our _onDeleteItem function:

_onSubmit(String s) {
  if (s.isNotEmpty) {
    items.add(Item(s));
    _textEditingController.clear();
    setState(() {});
  }
}

Try it, you should now be able to add new tasks and delete old ones.

Note to self: make more and better GIFs next time.

Note to self: make more and better GIFs next time.

And that’s it! With 100 lines of code we built a cross platform to-do app. You can check the whole code here.

The next step is making the data persistent by storing the items in the device’s storage. Stay tuned for the third part of this series!

¹ You can also close the PersistantModalSheet by dragging it down.

I love building products and I found my place to do so at Runtime Revolution. If you are interested in who we are and what we do, make sure to reach out!

Top comments (0)