Example: task manager
Learning objectives
- You know how to build a simple application that allows adding and listing tasks.
Let's look at building a task manager that has the functionality for creating and listing tasks. The full code for the application is available in the following box if you open it up.
Representing a task
To represent a task, we create a class Task
-- each task has a name and information on whether they are priority tasks.
class Task {
final String name;
final bool priority;
Task({required this.name, required this.priority});
}
A list of tasks in a provider
To separate creating and listing of tasks, let's handle tasks in a provider. As the data is a bit more complex, instead of directly using StateProvider
, we extend StateNotifier to provide means to interact with the state.
By detault, our TaskNotifier
is empty, and for our purposes, we simply need the functionality for adding tasks to the list. As notifications about changes in the state are sent only if the state changes, i.e. the actual value of state changes, we need to create a new list of tasks and set it as the value of state whenever a new task is added.
class TaskNotifier extends StateNotifier<List<Task>> {
TaskNotifier() : super([]);
addTask(Task task) {
state = [task, ...state];
}
}
With the TaskNotifier
, we can create a provider using StateNotifierProvider, which takes both the TaskNotifier
and List<Task>
(i.e. data that is managed by TaskNotifier
) as type parameters.
final taskProvider =
StateNotifierProvider<TaskNotifier, List<Task>>((ref) => TaskNotifier());
Form for adding tasks
To create a form for adding tasks, we would use a stateful widget. The form would have a text field and a switch -- the text field is used to input the name of the task, while the switch is used to indicate whether the task is a priority task.
One implementation of the form would be as follows. Note in particular that when watching the notifier from the taskProvider
, we can directly use the addTask
method.
class TaskForm extends ConsumerStatefulWidget {
ConsumerState<TaskForm> createState() => _TaskFormState();
}
class _TaskFormState extends ConsumerState<TaskForm> {
final _taskNameController = TextEditingController();
bool _priority = false;
_addTask() {
ref
.watch(taskProvider.notifier)
.addTask(Task(name: _taskNameController.text, priority: _priority));
_taskNameController.clear();
setState(() => _priority = false);
}
void dispose() {
_taskNameController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Column(children: [
TextField(
controller: _taskNameController,
decoration: const InputDecoration(
hintText: 'Task name', border: OutlineInputBorder()),
),
Row(children: [
Switch(
value: _priority,
onChanged: (bool value) {
setState(() => _priority = value);
}),
const Text('Priority')
]),
ElevatedButton(onPressed: _addTask, child: const Text('Add task')),
]);
}
}
Listing tasks
In addition, we would need a widget for listing the tasks. One possible implementation would a column containing a list of text items, built from the tasks in the provider.
class TaskList extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: ref
.watch(taskProvider)
.map((t) => Text('${t.name} (priority: ${t.priority})'))
.toList());
}
}
Starting the application
Finally, we would need an entrypoint for starting the application. In this case, it'd be meaningful to create a widget that consists of two widgets -- the form and the list. This would look as follows.
void main() {
runApp(ProviderScope(child: TaskApp()));
}
class TaskApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: Column(children: [TaskForm(), TaskList()])));
}
}
Flutter and Forms
In the case of larger forms or the need for more form logic, one would typically resort to using packages for building the form. One viable option is Reactive Forms, while another is Flutter Form Builder. Both of these offer a range of input fields and ready-made mechanisms for validating input.