Explore the implementation of core functionalities in a Flutter To-Do app, focusing on state management, CRUD operations, data persistence, and best practices for scalable app development.
In this section, we will delve into the implementation of core functionalities for a Flutter To-Do app. This involves managing state, performing CRUD operations, ensuring data persistence, and handling user interactions. By the end of this chapter, you will have a comprehensive understanding of how to build a functional To-Do app with Flutter, equipped with best practices and scalable architecture.
State management is a crucial aspect of any Flutter application. For a simple To-Do app, using Flutter’s built-in setState
method might be sufficient. However, as the app grows, you might want to consider more scalable solutions like Provider
, Riverpod
, or Bloc
.
Using setState
: Ideal for small applications where state changes are minimal and localized. It is straightforward but can become cumbersome as the app complexity increases.
Using Provider
: A popular choice for managing state in Flutter apps. It offers a more scalable approach by separating business logic from UI components, making the codebase more maintainable.
Here’s a simple example of using Provider
to manage a list of tasks:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TaskData(),
child: MyApp(),
),
);
}
class TaskData extends ChangeNotifier {
List<String> _tasks = [];
List<String> get tasks => _tasks;
void addTask(String task) {
_tasks.add(task);
notifyListeners();
}
void removeTask(int index) {
_tasks.removeAt(index);
notifyListeners();
}
}
The core of our To-Do app is the task list, which we will maintain in the app’s state. This list will be dynamically updated as users add, edit, or delete tasks.
CRUD (Create, Read, Update, Delete) operations form the backbone of our app’s functionality. Let’s explore how to implement each of these operations.
To add a new task, we need a user interface that allows users to input task details. We will then update the task list in the state.
class AddTaskScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
String newTaskTitle;
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
autofocus: true,
onChanged: (newText) {
newTaskTitle = newText;
},
),
FlatButton(
child: Text('Add'),
onPressed: () {
Provider.of<TaskData>(context, listen: false).addTask(newTaskTitle);
Navigator.pop(context);
},
),
],
),
);
}
}
Displaying tasks involves reading the current state and rendering it in the UI. We can use Flutter’s ListView
widget to dynamically display the list of tasks.
class TaskList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<TaskData>(
builder: (context, taskData, child) {
return ListView.builder(
itemCount: taskData.tasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(taskData.tasks[index]),
);
},
);
},
);
}
}
To update a task, we need to allow users to edit the task details and then update the state accordingly.
void updateTask(int index, String newTaskTitle) {
_tasks[index] = newTaskTitle;
notifyListeners();
}
Removing a task from the list is straightforward. We simply remove the task from the state and refresh the UI.
void removeTask(int index) {
_tasks.removeAt(index);
notifyListeners();
}
To ensure that tasks are not lost when the app is closed, we need to implement data persistence. There are several options available in Flutter for local storage.
SharedPreferences
: Suitable for simple key-value storage. Ideal for storing small amounts of data like user preferences.
sqflite
: A SQLite plugin for Flutter. It is a good choice for more complex data storage needs.
hive
: A lightweight and fast NoSQL database for Flutter. It is easy to use and integrates well with Flutter apps.
Let’s use SharedPreferences
to persist our task list. This involves saving the task list whenever changes occur and loading it when the app starts.
import 'package:shared_preferences/shared_preferences.dart';
class TaskData extends ChangeNotifier {
List<String> _tasks = [];
List<String> get tasks => _tasks;
Future<void> loadTasks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_tasks = prefs.getStringList('tasks') ?? [];
notifyListeners();
}
Future<void> saveTasks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setStringList('tasks', _tasks);
}
void addTask(String task) {
_tasks.add(task);
saveTasks();
notifyListeners();
}
void removeTask(int index) {
_tasks.removeAt(index);
saveTasks();
notifyListeners();
}
}
To mark tasks as completed, we can add a boolean property to each task and update the UI accordingly.
class Task {
final String name;
bool isDone;
Task({this.name, this.isDone = false});
void toggleDone() {
isDone = !isDone;
}
}
Update the UI to reflect the completed status:
ListTile(
title: Text(
task.name,
style: TextStyle(
decoration: task.isDone ? TextDecoration.lineThrough : null,
),
),
trailing: Checkbox(
value: task.isDone,
onChanged: (bool newValue) {
task.toggleDone();
notifyListeners();
},
),
)
Ensure that users cannot add empty tasks by validating input before updating the state.
if (newTaskTitle != null && newTaskTitle.isNotEmpty) {
Provider.of<TaskData>(context, listen: false).addTask(newTaskTitle);
}
Handle exceptions when interacting with local storage to prevent app crashes.
try {
prefs.setStringList('tasks', _tasks);
} catch (e) {
print('Failed to save tasks: $e');
}
Below is a flowchart illustrating the logic flow for adding, updating, and deleting tasks.
graph TD; A[Start] --> B{Add Task?}; B -->|Yes| C[Input Task Details]; C --> D[Update State]; D --> E[Save to Storage]; E --> F[Refresh UI]; B -->|No| G{Update/Delete Task?}; G -->|Yes| H[Select Task]; H --> I[Update/Delete State]; I --> J[Save to Storage]; J --> K[Refresh UI]; G -->|No| L[End];
The following diagram shows how state changes impact the UI.
sequenceDiagram participant User participant UI participant State participant Storage User->>UI: Add Task UI->>State: Update Task List State->>Storage: Save Task List Storage-->>State: Confirmation State->>UI: Refresh UI
Keep business logic separate from UI code to enhance maintainability and scalability. Use models and services to handle data operations.
Structure your code to facilitate future feature additions. Use design patterns like MVVM (Model-View-ViewModel) or MVC (Model-View-Controller) to organize your codebase.
By following the steps outlined in this chapter, you have implemented a functional To-Do app in Flutter. This app demonstrates key concepts such as state management, CRUD operations, data persistence, and error handling. As you continue to develop your app, consider exploring more advanced features and optimizations.