Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/lesnitsky/todolist_flutter

πŸŽ“Flutter TodoList tutorial
https://github.com/lesnitsky/todolist_flutter

flutter todolist tutorial workshop

Last synced: 5 days ago
JSON representation

πŸŽ“Flutter TodoList tutorial

Awesome Lists containing this project

README

        

# Todo List built with Flutter


Awesome Flutter

> Built with [Git Tutor](https://github.com/lesnitsky/git-tutor)

[](https://twitter.com/lesnitsky_dev)
[](https://github.com/lesnitsky/todolist_flutter)

This tutorial will walk you through the process of building of a simple todo-list with Flutter

## Getting started

Make sure to complete [flutter installation](https://flutter.io/docs/get-started/install)

## First steps

Execute in your terminal

```sh
flutter create todo_list
```

First line is an import of `material` library provided by Flutter. This library is an implementation of various android components

πŸ“„ lib/main.dart

```diff
+ import 'package:flutter/material.dart';

```

This function is an entry point of flutter application. It calls just `runApp`, but we can do more in this function (like making your application full-screen).

πŸ“„ lib/main.dart

```diff
import 'package:flutter/material.dart';
+
+ void main() => runApp(MyApp());

```

Let's actually do this 😏

πŸ“„ lib/main.dart

```diff
import 'package:flutter/material.dart';
+ import 'package:flutter/services.dart';

- void main() => runApp(MyApp());
+ void main() {
+ SystemChrome.setEnabledSystemUIOverlays([]);
+ runApp(MyApp());
+ }

```

Every component in flutter is called `widget`. It could be either `stateless` (read - pure) or `stateful` (container for some state). Top-level app component should be a stateless components, so let's create one

πŸ“„ lib/main.dart

```diff
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
+
+ class MyApp extends StatelessWidget {}

```

Every widget should override `build` function. It returns a hierarchy of your layout widgets (`Container`, `Padding`, `Flex`, etc) or your `stateful` widgets which contain some business logic

πŸ“„ lib/main.dart

```diff
runApp(MyApp());
}

- class MyApp extends StatelessWidget {}
+ class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+ }

```

But in case of top-level App widget, it should return either `CupertinoApp` from `'package:flutter/cupertino.dart'`, or `MaterialApp` from `'package:flutter/material.dart'`

We'll use `material` in this tutorial

πŸ“„ lib/main.dart

```diff
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container();
+ return MaterialApp();
}
}

```

Let's add title

πŸ“„ lib/main.dart

```diff
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp();
+ return MaterialApp(
+ title: 'Todo List',
+ );
}
}

```

Let's also make a `Scaffold` a home of our application

`Scaffold` is a helper class from `material` library which implements basic app layout (app bar, floating action button)

πŸ“„ lib/main.dart

```diff
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
+ home: Scaffold(
+ ),
);
}
}

```

Now we need to add an application header which will display our app title

πŸ“„ lib/main.dart

```diff
return MaterialApp(
title: 'Todo List',
home: Scaffold(
+ appBar: AppBar(title: Text('Todo List')),
),
);
}

```

And finally the body of our app is todolist itself. Let's just add this line and implement the class later

πŸ“„ lib/main.dart

```diff
title: 'Todo List',
home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
+ body: TodoList(),
),
);
}

```

## Render list

Basic statefull widget will look like this

πŸ“„ lib/todo_list.dart

```dart
import 'package:flutter/material.dart';

class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}

class _TodoListState extends State {
@override
Widget build(BuildContext context) {
return Container();
}
}

```

We also need to import our `TodoList` widget

πŸ“„ lib/main.dart

```diff
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

+ import 'package:todo_list/todo_list.dart';
+
void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());

```

Now let's describe `Todo` entity as class

πŸ“„ lib/todo.dart

```dart
class Todo {
Todo({this.title, this.isDone = false});

String title;
bool isDone;
}

```

and import it to `TodoList`

πŸ“„ lib/todo_list.dart

```diff
import 'package:flutter/material.dart';
+ import 'package:todo_list/todo.dart';

class TodoList extends StatefulWidget {
@override

```

Now we need to extend our `TodoList` state and add a list of todos

πŸ“„ lib/todo_list.dart

```diff
}

class _TodoListState extends State {
+ List todos = [];
+
@override
Widget build(BuildContext context) {
return Container();

```

Let's use `ListView` to render our todo items.

πŸ“„ lib/todo_list.dart

```diff
class _TodoListState extends State {
List todos = [];

+ _buildItem() {}
+
@override
Widget build(BuildContext context) {
- return Container();
+ return ListView.builder(
+ itemBuilder: _buildItem,
+ itemCount: todos.length,
+ );
}
}

```

Now we're going to implement `_buildItem` which will be called each time todo has to be rendered

We'll use `CheckboxListTile` from `material` library as it has everything we need (checkbox indicating whether todo is completed and title)

πŸ“„ lib/todo_list.dart

```diff
class _TodoListState extends State {
List todos = [];

- _buildItem() {}
+ Widget _buildItem(BuildContext context, int index) {
+ final todo = todos[index];
+
+ return CheckboxListTile(
+ );
+ }

@override
Widget build(BuildContext context) {

```

Value indicates if list item should be checked

πŸ“„ lib/todo_list.dart

```diff
final todo = todos[index];

return CheckboxListTile(
+ value: todo.isDone,
);
}

```

Title is a widget which should be rendered in first row. Typically it is a `Text` widget

πŸ“„ lib/todo_list.dart

```diff

return CheckboxListTile(
value: todo.isDone,
+ title: Text(todo.title),
);
}

```

Finally we need to handle taps on every list item

πŸ“„ lib/todo_list.dart

```diff
return CheckboxListTile(
value: todo.isDone,
title: Text(todo.title),
+ onChanged: (bool isChecked) {
+ _toggleTodo(todo, isChecked);
+ },
);
}

```

`_toggleTodo` implementation is pretty straightforward

πŸ“„ lib/todo_list.dart

```diff
class _TodoListState extends State {
List todos = [];

+ _toggleTodo(Todo todo, bool isChecked) {
+ todo.isDone = isChecked;
+ }
+
Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];

```

Let's try to add some mock todos and see if they are rendered correctly

πŸ“„ lib/todo_list.dart

```diff
}

class _TodoListState extends State {
- List todos = [];
+ List todos = [
+ Todo(title: 'Learn Dart'),
+ Todo(title: 'Try Flutter'),
+ Todo(title: 'Be amazed'),
+ ];

_toggleTodo(Todo todo, bool isChecked) {
todo.isDone = isChecked;

```

Ok, everything is rendered correctly, but nothing happens when we tap on items, weird..

Let's add a debug print and see if the handler even called

πŸ“„ lib/todo_list.dart

```diff
];

_toggleTodo(Todo todo, bool isChecked) {
+ print('${todo.title} ${todo.isDone}');
+
todo.isDone = isChecked;
}

```

Console shows items are checked, value `isChecked` is `true`, but checkbox is never rendered

The problem is that we modify our entities, but flutter has no idea this happened, so we need to call `setState`. (Hi there, react fans! 😏)

πŸ“„ lib/todo_list.dart

```diff
];

_toggleTodo(Todo todo, bool isChecked) {
- print('${todo.title} ${todo.isDone}');
-
- todo.isDone = isChecked;
+ setState(() {
+ todo.isDone = isChecked;
+ });
}

Widget _buildItem(BuildContext context, int index) {

```

Now we're good with rendering and updates, time to get rid of mock items and add some ui to add new todos.

Let's add a `FloatingActionButton`

πŸ“„ lib/main.dart

```diff
home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: TodoList(),
+ floatingActionButton: FloatingActionButton(
+ child: Icon(Icons.add),
+ ),
),
);
}

```

πŸ“„ lib/todo_list.dart

```diff
}

class _TodoListState extends State {
- List todos = [
- Todo(title: 'Learn Dart'),
- Todo(title: 'Try Flutter'),
- Todo(title: 'Be amazed'),
- ];
+ List todos = [];

_toggleTodo(Todo todo, bool isChecked) {
setState(() {

```

Ok, but what should we do in `onPressed`? We need to access a state of `TodoList` and messing with children state directly from parent _statelsess_ widget doesn't sound like a good idea

πŸ“„ lib/main.dart

```diff
body: TodoList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
+ onPressed: () {
+ // 😒
+ },
),
),
);

```

So let's just move `Scaffold` widget down to `TodoList`

πŸ“„ lib/main.dart

```diff
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
- home: Scaffold(
- appBar: AppBar(title: Text('Todo List')),
- body: TodoList(),
- floatingActionButton: FloatingActionButton(
- child: Icon(Icons.add),
- onPressed: () {
- // 😒
- },
- ),
- ),
+ home: TodoList(),
);
}
}

```

πŸ“„ lib/todo_list.dart

```diff
);
}

+ _addTodo() {}
+
@override
Widget build(BuildContext context) {
- return ListView.builder(
- itemBuilder: _buildItem,
- itemCount: todos.length,
+ return Scaffold(
+ appBar: AppBar(title: Text('Todo List')),
+ body: ListView.builder(
+ itemBuilder: _buildItem,
+ itemCount: todos.length,
+ ),
+ floatingActionButton: FloatingActionButton(
+ child: Icon(Icons.add),
+ onPressed: _addTodo,
+ ),
);
}
}

```

Now we can show a dialog when user taps on `FloatingActionButton`

πŸ“„ lib/todo_list.dart

```diff
);
}

- _addTodo() {}
+ _addTodo() {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text('New todo'),
+ );
+ },
+ );
+ }

@override
Widget build(BuildContext context) {

```

Dialog will contain a text input:

πŸ“„ lib/todo_list.dart

```diff
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
+ content: TextField(),
);
},
);

```

and two action buttons: `Cancel` and `Add`

πŸ“„ lib/todo_list.dart

```diff
return AlertDialog(
title: Text('New todo'),
content: TextField(),
+ actions: [
+ FlatButton(
+ child: Text('Cancel'),
+ ),
+ FlatButton(
+ child: Text('Add'),
+ ),
+ ],
);
},
);

```

Dialogs are not just overlays, but actually a routes, so to handle `Cancel` action we can just call `.pop` on `Navigator` of current `context`

πŸ“„ lib/todo_list.dart

```diff
actions: [
FlatButton(
child: Text('Cancel'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
),
FlatButton(
child: Text('Add'),

```

Now we need to access the value of a `TextField` to create a `Todo`
To do this we need to create a `TextEditingController`

πŸ“„ lib/todo_list.dart

```diff
class _TodoListState extends State {
List todos = [];

+ TextEditingController controller = new TextEditingController();
+
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;

```

and supply it to the `TextField`

πŸ“„ lib/todo_list.dart

```diff
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
- content: TextField(),
+ content: TextField(controller: controller),
actions: [
FlatButton(
child: Text('Cancel'),

```

now in `onPressed` of `Add` action we can log the value of a `TextField` and clear it

πŸ“„ lib/todo_list.dart

```diff
),
FlatButton(
child: Text('Add'),
+ onPressed: () {
+ print(controller.value.text);
+ controller.clear();
+ },
),
],
);

```

Finally let's actually create new todo and add it to the list of existing todos (don't forget to wrap the code with `setState`)

πŸ“„ lib/todo_list.dart

```diff
FlatButton(
child: Text('Add'),
onPressed: () {
- print(controller.value.text);
- controller.clear();
+ setState(() {
+ final todo = new Todo(title: controller.value.text);
+
+ todos.add(todo);
+ controller.clear();
+
+ Navigator.of(context).pop();
+ });
},
),
],

```

Tiny UX improvement: make keyboard pop automatically by passing `autofocus: true` to a `TextField`

πŸ“„ lib/todo_list.dart

```diff
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
- content: TextField(controller: controller),
+ content: TextField(
+ controller: controller,
+ autofocus: true,
+ ),
actions: [
FlatButton(
child: Text('Cancel'),

```

## Refactoring

`TodoList`is working, but `todo_list.dart` is kinda messy and hard to read. The most complex method is `_addTodo`, so let's start with rewriting it. It seems like we can move the `AlertDialog` to a separate widget, but we can't do this right now, as we rely on `setState` from parent widget. Instead we can pass a freshly created todo to a `Navigator.pop`

πŸ“„ lib/todo_list.dart

```diff
}

_addTodo() {
- showDialog(
+ showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
FlatButton(
child: Text('Add'),
onPressed: () {
- setState(() {
- final todo = new Todo(title: controller.value.text);
+ final todo = new Todo(title: controller.value.text);
+ controller.clear();

- todos.add(todo);
- controller.clear();
-
- Navigator.of(context).pop();
- });
+ Navigator.of(context).pop(todo);
},
),
],

```

In order to be able to receive the `Todo` in `_addTodo` method we need to make it `async` and `await` `showDialog` function result (which will be `null` in case it was dismissed and instance of `Todo` otherwise)

πŸ“„ lib/todo_list.dart

```diff
);
}

- _addTodo() {
- showDialog(
+ _addTodo() async {
+ final todo = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(

```

And move back the logic with state update

πŸ“„ lib/todo_list.dart

```diff
);
},
);
+
+ if (todo != null) {
+ setState(() {
+ todos.add(todo);
+ });
+ }
}

@override

```

Now we don't have any dependencies on a parent widget, so we can extract `AlertDialog` to a separate widget

πŸ“„ lib/new_todo_dialog.dart

```dart
import 'package:flutter/material.dart';

import 'package:todo_list/todo.dart';

class NewTodoDialog extends StatelessWidget {
final controller = new TextEditingController();

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('New todo'),
content: TextField(
controller: controller,
autofocus: true,
),
actions: [
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Add'),
onPressed: () {
final todo = new Todo(title: controller.value.text);
controller.clear();

Navigator.of(context).pop(todo);
},
),
],
);
}
}

```

and use it inside `TodoList`

πŸ“„ lib/todo_list.dart

```diff
import 'package:flutter/material.dart';
import 'package:todo_list/todo.dart';

+ import 'package:todo_list/new_todo_dialog.dart';
+
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
class _TodoListState extends State {
List todos = [];

- TextEditingController controller = new TextEditingController();
-
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
final todo = await showDialog(
context: context,
builder: (BuildContext context) {
- return AlertDialog(
- title: Text('New todo'),
- content: TextField(
- controller: controller,
- autofocus: true,
- ),
- actions: [
- FlatButton(
- child: Text('Cancel'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- FlatButton(
- child: Text('Add'),
- onPressed: () {
- final todo = new Todo(title: controller.value.text);
- controller.clear();
-
- Navigator.of(context).pop(todo);
- },
- ),
- ],
- );
+ return NewTodoDialog();
},
);

```

Next step – extract todo list component

List istself could also be treated as stateless widget, state related logic could be handled by parent

So let's first rename `TodoList` to `TodoListScreen`

πŸ“„ lib/todo_list.dart

```diff

import 'package:todo_list/new_todo_dialog.dart';

- class TodoList extends StatefulWidget {
+ class TodoListScreen extends StatefulWidget {
@override
- _TodoListState createState() => _TodoListState();
+ _TodoListScreenState createState() => _TodoListScreenState();
}

- class _TodoListState extends State {
+ class _TodoListScreenState extends State {
List todos = [];

_toggleTodo(Todo todo, bool isChecked) {

```

rename file

πŸ“„ lib/todo_list_screen.dart => lib/todo_list.dart

and fix import

πŸ“„ lib/main.dart

```diff
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

- import 'package:todo_list/todo_list.dart';
+ import 'package:todo_list/todo_list_screen.dart';

void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
- home: TodoList(),
+ home: TodoListScreen(),
);
}
}

```

Let's move list related logic to a separate stateless widget

πŸ“„ lib/todo_list.dart

```dart
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
});
}

Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];

return CheckboxListTile(
value: todo.isDone,
title: Text(todo.title),
onChanged: (bool isChecked) {
_toggleTodo(todo, isChecked);
},
);
}

@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: _buildItem,
itemCount: todos.length,
);
}
}

```

and remove this logic from `TodoListScreen`

πŸ“„ lib/todo_list_screen.dart

```diff
import 'package:todo_list/todo.dart';

import 'package:todo_list/new_todo_dialog.dart';
+ import 'package:todo_list/todo_list.dart';

class TodoListScreen extends StatefulWidget {
@override
class _TodoListScreenState extends State {
List todos = [];

- _toggleTodo(Todo todo, bool isChecked) {
- setState(() {
- todo.isDone = isChecked;
- });
- }
-
- Widget _buildItem(BuildContext context, int index) {
- final todo = todos[index];
-
- return CheckboxListTile(
- value: todo.isDone,
- title: Text(todo.title),
- onChanged: (bool isChecked) {
- _toggleTodo(todo, isChecked);
- },
- );
- }
-
_addTodo() async {
final todo = await showDialog(
context: context,
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
- body: ListView.builder(
- itemBuilder: _buildItem,
- itemCount: todos.length,
- ),
+ body: TodoList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addTodo,

```

Now let's review our `TodoList` widget

It is missing `Todo` class import

πŸ“„ lib/todo_list.dart

```diff
import 'package:flutter/material.dart';

+ import 'package:todo_list/todo.dart';
+
class TodoList extends StatelessWidget {
_toggleTodo(Todo todo, bool isChecked) {
setState(() {

```

It also doesn't have `todos`, so let's pass them from parent widget

πŸ“„ lib/todo_list.dart

```diff
import 'package:todo_list/todo.dart';

class TodoList extends StatelessWidget {
+ TodoList({@required this.todos});
+
+ final List todos;
+
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;

```

πŸ“„ lib/todo_list_screen.dart

```diff
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
- body: TodoList(),
+ body: TodoList(
+ todos: todos,
+ ),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addTodo,

```

`_toggleTodo` method relies on `setState`, so let's move it back to parent

πŸ“„ lib/todo_list.dart

```diff

final List todos;

- _toggleTodo(Todo todo, bool isChecked) {
- setState(() {
- todo.isDone = isChecked;
- });
- }
-
Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];

```

πŸ“„ lib/todo_list_screen.dart

```diff
class _TodoListScreenState extends State {
List todos = [];

+ _toggleTodo(Todo todo, bool isChecked) {
+ setState(() {
+ todo.isDone = isChecked;
+ });
+ }
+
_addTodo() async {
final todo = await showDialog(
context: context,

```

and pass it down to `TodoList` as a property

πŸ“„ lib/todo_list.dart

```diff

import 'package:todo_list/todo.dart';

+ typedef ToggleTodoCallback = void Function(Todo, bool);
+
class TodoList extends StatelessWidget {
- TodoList({@required this.todos});
+ TodoList({@required this.todos, this.onTodoToggle});

final List todos;
+ final ToggleTodoCallback onTodoToggle;

Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];
value: todo.isDone,
title: Text(todo.title),
onChanged: (bool isChecked) {
- _toggleTodo(todo, isChecked);
+ onTodoToggle(todo, isChecked);
},
);
}

```

πŸ“„ lib/todo_list_screen.dart

```diff
appBar: AppBar(title: Text('Todo List')),
body: TodoList(
todos: todos,
+ onTodoToggle: _toggleTodo,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),

```

## Conclusion

Yay! We have working and kinda well-structured Todo List application written in Flutter πŸŽ‰

But there is still a lot of work to do:

![App-Screenshot-3.png](https://s3.eu-west-2.amazonaws.com/git-tutor-assets/git-tutor-todolist-3.png)

See you in next tutorials! πŸ‘‹

> Built with [Git Tutor](https://github.com/lesnitsky/git-tutor)

[](https://twitter.com/lesnitsky_dev)
[](https://github.com/lesnitsky/todolist_flutter)