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 applicationthe
MyApp
class, that holds the root of the widget treeMaterialApp()
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 aContainer()
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.
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),
),
);
}
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.
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)