Building Smart Task Managers with Lists, Maps, and ListView in Flutter

Maxim Gorin
7 min readSep 12, 2024

--

In any app, collections of data play a vital role, whether it’s managing tasks, storing user information, or displaying a list of products. Working with collections like lists and maps in Dart allows you to efficiently manage, manipulate, and display data in your Flutter apps.

This beginner-friendly series is designed for those with no prior programming experience, guiding you through Flutter’s fundamentals step-by-step, all while practicing in real-time using DartPad, without needing any setup or installation.

In our previous article, Your First Flutter Forms: Creating and Validating User Input, we learned how to capture and validate user input. Now, we’re going to dive into the world of collections and learn how to display dynamic data using `ListView` in Flutter.

Learn Flutter any way you want

Understanding Lists and Maps in Dart

Lists in Dart

A List is a collection of ordered data where each element can be accessed by its index. In Dart, lists are flexible and can store any data type, whether it’s numbers, text, or even objects. Lists are important when you want to manage a group of items, such as a list of tasks, users, or products.

Here’s an example of a simple list in Dart:

List<String> tasks = [
'Buy groceries',
'Walk the dog',
'Write Flutter article',
];

print(tasks[0]);
print(tasks[2]);

Each item in the list can be accessed by its index, starting from zero. For example, tasks[0] would return 'Buy groceries'.

Lists in Dart

Maps in Dart

A Map is another collection in Dart that stores data as key-value pairs. While lists are great for ordered data, maps shine when you need to associate data with unique keys. Think of maps as a dictionary where you can look up values based on a specific key.

Here’s an example of a map in Dart:

Map<String, String> taskDetails = {
'Buy groceries': 'Get milk, eggs, and bread',
'Walk the dog': 'Take Sparky to the park',
'Write Flutter article': 'Draft article on collections and lists'
};

print(taskDetails['Walk the dog']);

In this case, you can access the details of a task using its key, like taskDetails['Buy groceries'], which would return 'Get milk, eggs, and bread'.

Maps in Dart

Displaying Data Using ListView in Flutter

Now that we understand how to work with lists and maps in Dart, let’s move on to displaying this data in Flutter. ListView is one of the most powerful widgets in Flutter for displaying collections of data. It allows you to create scrollable lists, which is perfect for showing a large number of items.

Creating a Simple To-Do List with ListView

Simple To-Do List

In this section, we’ll create a simple to-do list app that allows users to add tasks and display them using ListView. Here's the code:

import 'package:flutter/material.dart';

class TodoApp extends StatefulWidget {
@override
_TodoAppState createState() => _TodoAppState();
}

class _TodoAppState extends State<TodoApp> {
final TextEditingController _taskController = TextEditingController();
final List<String> _tasks = [];

void _addTask() {
setState(() {
if (_taskController.text.isNotEmpty) {
_tasks.add(_taskController.text);
_taskController.clear();
}
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('To-Do List')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _taskController,
decoration: InputDecoration(
labelText: 'Enter a task',
border: OutlineInputBorder(),
),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _addTask,
child: Text('Add Task'),
),
Expanded(
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_tasks[index]),
);
},
),
),
],
),
),
);
}
}

void main() => runApp(MaterialApp(home: TodoApp()));

Explanation of Key Concepts

  • TextEditingController: This controller is used to capture the user’s input from the text field. Once the user enters a task and presses the “Add Task” button, the text is added to the list of tasks.
  • setState(): This function triggers a rebuild of the UI whenever the state changes. In this case, when a new task is added, the UI updates to display the new task.
  • ListView.builder: This widget is essential for building scrollable lists. It only renders the items currently visible on the screen, making it efficient even for long lists. The itemBuilder function generates each item in the list based on its index.
  • ListTile: This widget provides a simple way to display a list item, in this case, a single task.

Enhanced To-Do List with Task Completion, Reversion, and Deletion

Enhanced To-Do List

Let’s extend our app to include more detailed information about each task, such as a description. For this, we’ll use a Map to store the task title and its corresponding description.

import 'package:flutter/material.dart';

class EnhancedTodoApp extends StatefulWidget {
@override
_EnhancedTodoAppState createState() => _EnhancedTodoAppState();
}

class _EnhancedTodoAppState extends State<EnhancedTodoApp> {
final TextEditingController _taskController = TextEditingController();
final List<Map<String, dynamic>> _tasks = []; // List of active tasks
final List<Map<String, dynamic>> _completedTasks = []; // List of completed tasks

void _addTask() {
setState(() {
if (_taskController.text.isNotEmpty) {
_tasks.add({'task': _taskController.text, 'completed': false});
_taskController.clear();
}
});
}

void _toggleTaskCompletion(int index, bool isCompletedList) {
setState(() {
if (isCompletedList) {
final task = _completedTasks[index];
task['completed'] = false;
_tasks.add(task); // Move back to active tasks
_completedTasks.removeAt(index);
} else {
final task = _tasks[index];
task['completed'] = true;
_completedTasks.add(task); // Move to completed tasks
_tasks.removeAt(index);
}
});
}

void _deleteTask(int index, bool isCompletedList) {
setState(() {
if (isCompletedList) {
_completedTasks.removeAt(index);
} else {
_tasks.removeAt(index);
}
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Enhanced To-Do List')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _taskController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter a task',
),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _addTask,
child: Text('Add Task'),
),
SizedBox(height: 20),
Text('Active Tasks', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Expanded(
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
return ListTile(
leading: Checkbox(
value: _tasks[index]['completed'],
onChanged: (_) => _toggleTaskCompletion(index, false),
),
title: Text(_tasks[index]['task']),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteTask(index, false),
),
);
},
),
),
Divider(),
Text('Completed Tasks', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Expanded(
child: ListView.builder(
itemCount: _completedTasks.length,
itemBuilder: (context, index) {
return ListTile(
leading: Checkbox(
value: _completedTasks[index]['completed'],
onChanged: (_) => _toggleTaskCompletion(index, true), // Move task back to active
),
title: Text(
_completedTasks[index]['task'],
style: TextStyle(
decoration: TextDecoration.lineThrough, // Line-through for completed tasks
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteTask(index, true),
),
);
},
),
),
],
),
),
);
}
}

void main() => runApp(MaterialApp(home: EnhancedTodoApp()));

Explanation of Changes

Checkbox Placement:

  • The checkbox is now positioned before the text (as the leading widget of the ListTile), making it more intuitive for users to check off tasks.

Checkbox for Completed Tasks:

  • A checkbox is now included for tasks in the “Completed Tasks” section. When unchecked, the task is moved back to the “Active Tasks” list, reverting its completion status.

Task Movement Between Lists:

  • The setState function is used to move tasks between the active and completed lists depending on the checkbox status. If the checkbox in the active list is checked, the task is marked as completed and moved to the completed list. Similarly, unchecking a task in the completed list moves it back to the active list.

How the Updated App Works

Adding Tasks

  • Users can enter a task in the text field and press the “Add Task” button to add it to the active task list.

Marking Tasks as Complete/Incomplete:

  • Users can mark a task as complete by checking the checkbox. The task will then move to the completed section and will have a line-through effect. If they uncheck the checkbox in the completed tasks, the task will move back to the active tasks.

Deleting Tasks:

  • Tasks can be deleted from both the active and completed task lists using the delete icon.

Conclusion

Working with collections and displaying them dynamically is a fundamental part of building functional apps. By mastering lists, maps, and Flutter’s ListView, you can handle and present large amounts of data efficiently.

In this article, we built an enhanced to-do list app provides a more practical and dynamic experience by allowing users to mark tasks as completed, revert completed tasks to active, and delete tasks when no longer needed. With features like checkboxes and deletion, the app now offers a more complete and functional way to manage tasks, making it a useful tool for real-world use cases.

By continually experimenting with these features and expanding your app’s functionality, you’re not just building better software — you’re also sharpening your problem-solving skills and growing as a developer. Keep pushing your creativity, and soon enough, you’ll be able to tackle even more complex and rewarding projects with confidence.

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

No responses yet