Design Converter
Education
Last updated on Jun 3, 2024
Last updated on Jun 3, 2024
Every day, we juggle a myriad of tasks, deadlines, and priorities. Having a handy tool to manage them effectively can significantly boost our productivity and peace of mind. This is precisely where a Flutter ToDo App comes into play, providing a structured framework to organize our daily activities.
In this blog, we will delve into creating a Flutter ToDo App step by step, employing robust state management techniques to ensure a smooth and interactive user experience.
Not only will this todo app tutorial equip you with the practical skills to build your own Flutter ToDo App, but it will also deepen your understanding of the Flutter framework and state management principles. By the end of this journey, you'll possess a fully functional todo app that you can extend, customize, and potentially release to the world.
Flutter is an open-source UI software development kit created by Google. It's used to develop cross-platform applications for Android, iOS, Linux, Mac, Windows, Google Fuchsia, and the web from a single codebase. The beauty of developing an app with Flutter lies in its write once, run everywhere philosophy, saving you time and resources during development.
State management is the technique used to manage the state of an app across its different components. It's a crucial concept in Flutter, especially when developing a project like a Flutter ToDo App where you need to track the tasks' states (completed, pending, or deleted). The state of your app will determine what the UI displays at any moment, allowing for dynamic and responsive app behavior.
Before we embark on creating your first Flutter ToDo App, we need to set up our development environment. Begin by installing the Flutter SDK, and make sure to choose a preferred code editor—a popular choice is Visual Studio Code for its robust Flutter support.
Initialize your new Flutter project by opening your command line tool of choice, navigating to the folder where you want to create your project, and run the following command:
1flutter create todo_app
This command creates a new Flutter project with all the necessary boilerplate code to kickstart your todo app Flutter development.
A task class serves as a blueprint for the tasks in our app. Define this class by specifying properties such as final String title to maintain the task’s description, final String id for unique identification, and bool completed to track the task's completion status. This modularity and clarity will aid in managing tasks efficiently.
Inside your Flutter ToDo App project, create a new file called 'task.dart', and define your task class as follows:
1class Task { 2 final String id; 3 final String title; 4 bool completed; 5 6 Task({required this.id, required this.title, this.completed = false}); 7 8 void toggleCompleted() { 9 completed = !completed; 10 } 11}
Here, toggleCompleted is an instance method providing task functionality to mark a task as complete or incomplete.
Now, let's craft the user interface for our simple todo app. We want an interface that is clean and intuitive, allowing users to input new tasks and view a list of existing tasks easily.
Open your main.dart file and define your main application widget. Use a StatefulWidget since our UI will change dynamically based on the user interactions. The build method should return a Scaffold widget that provides the basic material design visual layout structure.
In the build method, start by defining a FloatingActionButton which users will interact with to add new tasks. Inside your Scaffold, let's also add an AppBar for the title and a ListView.builder that will be used to display the list of tasks.
Here's an example of what the code could look like inside the StatefulWidget:
1class ToDoApp extends StatefulWidget { 2 3 _ToDoAppState createState() => _ToDoAppState(); 4} 5 6class _ToDoAppState extends State<ToDoApp> { 7 final List<Task> _tasks = []; 8 9 void _addNewTask(String title) { 10 final newTask = Task( 11 title: title, 12 id: DateTime.now().toString(), 13 ); 14 15 setState(() { 16 _tasks.add(newTask); 17 }); 18 } 19 20 Widget _buildTaskItem(BuildContext context, int index) { 21 return ListTile( 22 title: Text(_tasks[index].title), 23 leading: Icon(_tasks[index].completed ? Icons.check : Icons.circle), 24 onTap: () { 25 setState(() { 26 _tasks[index].toggleCompleted(); 27 }); 28 }, 29 ); 30 } 31 32 33 Widget build(BuildContext context) { 34 return Scaffold( 35 appBar: AppBar( 36 title: Text('Flutter ToDo App'), 37 ), 38 body: ListView.builder( 39 itemBuilder: _buildTaskItem, 40 itemCount: _tasks.length, 41 ), 42 floatingActionButton: FloatingActionButton( 43 onPressed: () { 44 // Add functionality to show an input dialog 45 }, 46 tooltip: 'Add Task', 47 child: Icon(Icons.add), 48 ), 49 ); 50 } 51} 52 53void main() { 54 runApp(MaterialApp( 55 home: ToDoApp(), 56 )); 57}
Inside addNewTask, we're adding a new task to our task list and calling setState to tell Flutter that it needs to rebuild the UI with the new task added. The buildTaskItem function returns a ListTile widget for each task item, providing a title and changeable leading icon to depict the task's completion status.
Progress till now:
Next, let's enhance our simple todo app by adding a dialog box where users can input task titles for their new tasks.
State management will ensure that when we add, update, or delete a task, the UI reflects these changes consistently. Since we're holding the state within the _ToDoAppState class, our StatefulWidget takes care of its state management.
Each time a task is added or toggled, we invoke setState, triggering a rebuild of the Widget and updating the visual elements to reflect the current state.
Here is a simple example of how this state is managed when a user wants to add a new task:
1void _startAddNewTask(BuildContext context) { 2 showDialog( 3 context: context, 4 builder: (ctx) { 5 String taskTitle = ''; 6 return AlertDialog( 7 title: Text('Add New Task'), 8 content: TextField( 9 onChanged: (value) { 10 taskTitle = value; 11 }, 12 decoration: InputDecoration( 13 labelText: 'Task Title', 14 ), 15 ), 16 actions: <Widget>[ 17 ElevatedButton( 18 child: Text('Cancel'), 19 onPressed: () { 20 Navigator.of(ctx).pop(); 21 }, 22 ), 23 ElevatedButton( 24 child: Text('Add'), 25 onPressed: () { 26 if(taskTitle.isNotEmpty) { 27 _addNewTask(taskTitle); 28 Navigator.of(ctx).pop(); 29 } 30 }, 31 ), 32 ], 33 ); 34 } 35 ); 36}
When the add button is pressed, the startAddNewTask function displays an alert dialog box with a text field, allowing users to type in their task's title. Once confirmed, addNewTask is invoked, the task is added, and the dialog box is closed.
Having set the stage for our state management, we are at the most exciting part: adding functionality.
Create a method called _showAddTaskDialog and call it when the FloatingActionButton is pressed:
1floatingActionButton: FloatingActionButton( 2 onPressed: () => _showAddTaskDialog(context), 3 tooltip: 'Add Task', 4 child: Icon(Icons.add), 5),
In the _showAddTaskDialog method, simply use the showDialog function with the AlertDialog widget incorporating a TextField, as demonstrated in the state management section.
Progress till now:
By now, the basic structure to add and display tasks in our Flutter ToDo App is ready. But let's make viewing tasks a bit more interactive. The ability to discern completed tasks from pending ones is crucial for any simple todo app.
Extend the _buildTaskItem widget with some additional UI to visually indicate the task completion state:
1Widget _buildTaskItem(BuildContext context, int index) { 2 return Card( 3 elevation: 4, 4 margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), 5 child: ListTile( 6 title: Text( 7 _tasks[index].title, 8 style: TextStyle( 9 decoration: _tasks[index].completed ? TextDecoration.lineThrough : null 10 ), 11 ), 12 leading: Icon( 13 _tasks[index].completed ? Icons.check_circle : Icons.radio_button_unchecked, 14 color: _tasks[index].completed ? Colors.green : null, 15 ), 16 trailing: IconButton( 17 icon: Icon(Icons.delete), 18 onPressed: () { 19 setState(() { 20 _tasks.removeAt(index); 21 }); 22 }, 23 ), 24 onTap: () { 25 setState(() { 26 _tasks[index].toggleCompleted(); 27 }); 28 }, 29 ), 30 ); 31}
The Card widget wraps our ListTile adding a nice elevation effect and some margins. Completed tasks are styled with TextDecoration.lineThrough, and we use distinct icons to represent different task states. The ability to delete tasks is introduced with a trailing IconButton that calls setState to update our todo list.
Progress till now:
The onTap method of ListTile toggles the completion status of a task. This change affects the representation of the task within the list, thanks to the toggleCompleted method defined in our task class.
The trailing IconButton on each ListTile listens for a press event to execute its deletion logic. When the button is pressed, the corresponding task is removed from the task array using the removeAt method, followed by a call to setState to update the UI.
Since the user might accidentally delete a task, let's provide a way to undo this operation. To do this, incorporate a SnackBar that temporarily appears on deletion and offers an undo action:
1void _deleteTask(int index) { 2 final removedTask = _tasks[index]; 3 setState(() { 4 _tasks.removeAt(index); 5 }); 6 7 ScaffoldMessenger.of(context).showSnackBar( 8 SnackBar( 9 content: Text('Task removed!'), 10 action: SnackBarAction( 11 label: 'UNDO', 12 onPressed: () { 13 setState(() { 14 _tasks.insert(index, removedTask); 15 }); 16 }, 17 ), 18 ), 19 ); 20}
And replace the onPressed action within the delete button with a call to _deleteTask(index).
Progress till now:
User interaction doesn't stop at just adding, checking off, and removing tasks. Subtle feedback mechanisms and animations can greatly enhance the overall experience.
One way to do this is by adding a simple animation to the task list when a task's completion state changes. Flutter's AnimatedList widget can be a good choice for it.
1// Within the _ToDoAppState class 2 3GlobalKey<AnimatedListState> _listKey = GlobalKey(); 4 5void _addNewTask(String title) { 6 // ... existing code ... 7 _listKey.currentState?.insertItem(_tasks.length - 1); 8} 9 10void _deleteTask(int index) { 11 // ... existing deletion logic ... 12 _listKey.currentState?.removeItem( 13 index, 14 (context, animation) => _buildTaskItemWithAnimation(context, index, animation), 15 ); 16}
With _listKey.currentState?.insertItem and removeItem, the list animates the addition and deletion of items.
Additionally, consider incorporating haptic feedback when performing sensitive operations. For instance, upon deleting a task, providing a physical sensation can confirm the user's action:
1void _deleteTask(int index) { 2 // ... existing deletion logic ... 3 HapticFeedback.heavyImpact(); // Trigger a haptic feedback 4}
Progress till now:
Storing our todo list items is crucial for a real-world application. While there are various ways to persist data in a Flutter App, such as using a local database or a REST API, we're going to use simple file storage for demonstration purposes.
Use the path_provider plugin to locate the correct local path and write your tasks to a file. You can do this in simple JSON format for easy retrieval and manipulation.
1dependencies: 2 flutter: 3 sdk: flutter 4 path_provider: ^2.1.3
Run flutter pub get in your terminal to install it. Then, create methods to read and write your todo items to a file:
1import 'dart:io'; 2import 'dart:convert'; 3import 'package:path_provider/path_provider.dart'; 4 5class Storage { 6 Future<String> get _localPath async { 7 final directory = await getApplicationDocumentsDirectory(); 8 return directory.path; 9 } 10 11 Future<File> get _localFile async { 12 final path = await _localPath; 13 return File('$path/tasks.json'); 14 } 15 16 Future<List<Task>> readTasks() async { 17 try { 18 final file = await _localFile; 19 // Read the file 20 String contents = await file.readAsString(); 21 List<dynamic> jsonData = jsonDecode(contents); 22 return jsonData.map((item) => Task.fromJson(item)).toList(); 23 } catch (e) { 24 // If encountering an error, return an empty list 25 return []; 26 } 27 } 28 29 Future<File> writeTasks(List<Task> tasks) async { 30 final file = await _localFile; 31 // Convert tasks to json and write to file 32 String json = jsonEncode(tasks.map((task) => task.toJson()).toList()); 33 return file.writeAsString(json); 34 } 35}
Add the below code to your previously created 'task.dart' file:
1// Convert a Task instance to a Map 2 Map<String, dynamic> toJson() => { 3 'id': id, 4 'title': title, 5 'completed': completed, 6 }; 7 8 // Convert a Map to a Task instance 9 factory Task.fromJson(Map<String, dynamic> json) => Task( 10 id: json['id'] as String, 11 title: json['title'] as String, 12 completed: json['completed'] as bool, 13 );
After adding JSON serialization to your Task class, you can update your _ToDoAppState to read from the file when the app launches and to write to it whenever tasks are added or removed:
1class _ToDoAppState extends State<ToDoApp> { 2 final List<Task> _tasks = []; 3 final Storage storage = Storage(); 4 5 6 void initState() { 7 super.initState(); 8 storage.readTasks().then((taskList) { 9 setState(() { 10 _tasks.addAll(taskList); 11 }); 12 }); 13 } 14 15 void _addNewTask(String title) { 16 // ... existing logic ... 17 storage.writeTasks(_tasks); 18 } 19 20 void _deleteTask(int index) { 21 // ... existing logic ... 22 storage.writeTasks(_tasks); 23 } 24 25 // ... rest of your code ... 26}
By now, you've added persistent state management to your Flutter ToDo App. Tasks added will be there when the user returns, and completed and deleted tasks will be reflected accurately.
Final app output:
Building a robust Flutter ToDo App requires thorough debugging and testing. Use Flutter's DevTools for debugging performance issues, and write unit and widget tests to verify app functionality.
Flutter's hot reload feature makes it easier to iterate on your todo app faster, allowing you to quickly test changes without restarting your app. Make sure to handle exceptions and errors gracefully to provide a better overall user experience.
Testing is crucial for ensuring that your new features work as expected. For instance, you can write a widget test to confirm tasks are added to the todo list when the add button is clicked:
1import 'package:flutter_test/flutter_test.dart'; 2import 'package:todo_app/main.dart'; 3 4void main() { 5 testWidgets('Add task to list test', (WidgetTester tester) async { 6 await tester.pumpWidget(ToDoApp()); 7 8 // Enter 'Test Task' into the TextField widget 9 await tester.enterText(find.byType(TextField), 'Test Task'); 10 11 // Tap the 'Add' button to add the task 12 await tester.tap(find.text('Add')); 13 14 await tester.pump(); // Rebuild the widget after the state has changed 15 16 // Verify that the item has been added to the list 17 expect(find.text('Test Task'), findsOneWidget); 18 }); 19}
Congratulations! You've built a complete Flutter ToDo App, complete with state management, user interaction, and data persistence. Your app lays a solid foundation on which you can build more complex real-world applications.
Consider extending your todo app Flutter with additional features like sorting, filtering, or syncing with a web server. With the skills gained from this todo app tutorial, you're now better equipped to tackle more sophisticated Flutter projects.
If you've found this tutorial helpful and want to explore more complex app development topics with Flutter, explore DhiWise blogs for Flutter. To get the complete app code I am attaching the GitHub repository . Happy coding!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.