In the previous lesson, we completed the basic page structure of the to-do list app. In this lesson, we will build on that foundation to implement data persistence, state management, interaction optimization, and dark mode adaptation, equipping the app with more comprehensive functionality and a better user experience.
I. Integrating Network Requests and Local Storage
1. Implementing Network Requests (using Dio)
First, add dependencies to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
dio: ^5.9.0
flutter_riverpod: ^2.6.1
hive: ^2.2.3
hive_flutter: ^1.1.0
intl: ^0.20.2
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.6.0
Create a network request utility class utils/network_utils.dart:
import 'package:dio/dio.dart';
import '../models/task_model.dart';
class NetworkUtils {
static final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/tasks',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
// Fetch all tasks
static Future<List<Task>> fetchTasks() async {
try {
final response = await _dio.get('/');
List<dynamic> data = response.data;
return data.map((json) => Task.fromJson(json)).toList();
} catch (e) {
throw Exception('Failed to fetch tasks: $e');
}
}
// Add a new task
static Future<Task> addTask(Task task) async {
try {
final response = await _dio.post('/', data: task.toJson());
return Task.fromJson(response.data);
} catch (e) {
throw Exception('Failed to add task: $e');
}
}
// Update a task
static Future<Task> updateTask(Task task) async {
try {
final response = await _dio.put('/${task.id}', data: task.toJson());
return Task.fromJson(response.data);
} catch (e) {
throw Exception('Failed to update task: $e');
}
}
// Delete a task
static Future<void> deleteTask(String id) async {
try {
await _dio.delete('/$id');
} catch (e) {
throw Exception('Failed to delete task: $e');
}
}
// Unified error message handling
static String getErrorMsg(dynamic e) {
if (e is DioException) {
if (e.type == DioExceptionType.connectionTimeout) {
return 'Network connection timed out. Please check your network.';
} else if (e.type == DioExceptionType.receiveTimeout) {
return 'Data reception timed out.';
} else if (e.type == DioExceptionType.badResponse) {
return 'Server error: ${e.response?.statusCode}';
} else if (e.type == DioExceptionType.connectionError) {
return 'Unable to connect to server. Please check your network.';
}
}
return 'Operation failed. Please try again later.';
}
}
Modify the Task model with manual JSON serialization (models/task_model.dart):
class Task {
final String id;
String title;
String content;
DateTime createTime;
DateTime? deadline;
bool isCompleted;
Task({
required this.id,
required this.title,
this.content = '',
DateTime? createTime,
this.deadline,
this.isCompleted = false,
}) : createTime = createTime ?? DateTime.now();
// Manual JSON serialization
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'createTime': createTime.millisecondsSinceEpoch,
'deadline': deadline?.millisecondsSinceEpoch,
'isCompleted': isCompleted,
};
}
// Manual deserialization from JSON
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'],
title: json['title'],
content: json['content'] ?? '',
createTime: DateTime.fromMillisecondsSinceEpoch(json['createTime']),
deadline: json['deadline'] != null
? DateTime.fromMillisecondsSinceEpoch(json['deadline'])
: null,
isCompleted: json['isCompleted'] ?? false,
);
}
}
2. Implementing Local Storage (Hive)
Create a Hive adapter (models/task_adapter.dart):
import 'package:hive/hive.dart';
import 'task_model.dart';
class TaskAdapter extends TypeAdapter<Task> {
@override
final typeId = 0;
@override
Task read(BinaryReader reader) {
final id = reader.readString();
final title = reader.readString();
final content = reader.readString();
final createTime = DateTime.fromMillisecondsSinceEpoch(reader.readInt());
final hasDeadline = reader.readBool();
DateTime? deadline;
if (hasDeadline) {
deadline = DateTime.fromMillisecondsSinceEpoch(reader.readInt());
}
final isCompleted = reader.readBool();
return Task(
id: id,
title: title,
content: content,
createTime: createTime,
deadline: deadline,
isCompleted: isCompleted,
);
}
@override
void write(BinaryWriter writer, Task obj) {
writer.writeString(obj.id);
writer.writeString(obj.title);
writer.writeString(obj.content);
writer.writeInt(obj.createTime.millisecondsSinceEpoch);
writer.writeBool(obj.deadline != null);
if (obj.deadline != null) {
writer.writeInt(obj.deadline!.millisecondsSinceEpoch);
}
writer.writeBool(obj.isCompleted);
}
}
Initialize Hive (main.dart):
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'models/task_model.dart';
import 'models/task_adapter.dart';
import 'pages/home_page.dart';
import 'utils/router.dart';
import 'providers/theme_provider.dart';
void main() async {
// Initialize Hive
await Hive.initFlutter();
// Register adapter
Hive.registerAdapter(TaskAdapter());
// Open task box
await Hive.openBox<Task>('tasks');
// Open settings box
await Hive.openBox('settings');
runApp(ProviderScope(child: const MyApp()));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return MaterialApp(
title: 'To-Do List',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
scaffoldBackgroundColor: Colors.white,
cardColor: Colors.white,
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black87),
bodyMedium: TextStyle(color: Colors.black54),
),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
scaffoldBackgroundColor: Colors.grey[900],
cardColor: Colors.grey[800],
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white70),
bodyMedium: TextStyle(color: Colors.white54),
),
),
themeMode: themeMode,
initialRoute: Router.home,
routes: Router.routes,
onGenerateRoute: Router.generateRoute,
debugShowCheckedModeBanner: false,
);
}
}
Create a local storage utility class (utils/storage_utils.dart):
import 'package:hive/hive.dart';
import '../models/task_model.dart';
class StorageUtils {
static Box<Task> get _taskBox => Hive.box<Task>('tasks');
// Save task list
static Future<void> saveTasks(List<Task> tasks) async {
await _taskBox.clear();
await _taskBox.addAll(tasks);
}
// Get all tasks
static List<Task> getTasks() => _taskBox.values.toList();
// Add a single task
static Future<void> addTask(Task task) async {
await _taskBox.put(task.id, task);
}
// Update a task
static Future<void> updateTask(Task task) async {
await _taskBox.put(task.id, task);
}
// Delete a task
static Future<void> deleteTask(String id) async {
await _taskBox.delete(id);
}
}
II. Implementing State Management (Riverpod)
Create a task state management class (providers/task_provider.dart):
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/task_model.dart';
import '../utils/network_utils.dart';
import '../utils/storage_utils.dart';
// Define task state data class, containing task list and loading state
class TaskState {
final List<Task> tasks;
final bool isLoading;
final String? errorMessage;
TaskState({
required this.tasks,
required this.isLoading,
this.errorMessage,
});
// Copy method for easy state updates
TaskState copyWith({
List<Task>? tasks,
bool? isLoading,
String? errorMessage,
}) {
return TaskState(
tasks: tasks ?? this.tasks,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
// Task state provider
final taskProvider = StateNotifierProvider<TaskNotifier, TaskState>((ref) {
return TaskNotifier();
});
class TaskNotifier extends StateNotifier<TaskState> {
TaskNotifier() : super(TaskState(tasks: [], isLoading: true)) {
_loadTasks(); // Load tasks on initialization
}
// Load tasks from local storage
Future<void> loadTasks() async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
// First load local data
final localTasks = StorageUtils.getTasks();
state = state.copyWith(tasks: localTasks);
// Then try to fetch latest data from network
final networkTasks = await NetworkUtils.fetchTasks();
state = state.copyWith(tasks: networkTasks);
await StorageUtils.saveTasks(networkTasks);
} catch (e) {
// Keep local data if network request fails (if available)
final errorMsg = NetworkUtils.getErrorMsg(e);
state = state.copyWith(
errorMessage: errorMsg,
isLoading: false,
);
} finally {
if (mounted) {
state = state.copyWith(isLoading: false);
}
}
}
// Add task - still save to local if network fails
Future<void> addTask(Task task, {bool skipNetwork = false}) async {
final currentTasks = List<Task>.from(state.tasks);
try {
// Update UI first
state = state.copyWith(
tasks: [...state.tasks, task],
errorMessage: null,
);
// Save to local storage (execute regardless of network success)
await StorageUtils.addTask(task);
// Return directly if network request is not needed
if (skipNetwork) {
return;
}
// Try to sync with network
final newTask = await NetworkUtils.addTask(task);
// Update with task returned from network (may contain server-generated ID)
state = state.copyWith(
tasks: state.tasks.map((t) => t.id == task.id ? newTask : t).toList(),
);
} catch (e) {
// Keep locally saved state if network fails, no rollback
state = state.copyWith(
errorMessage: NetworkUtils.getErrorMsg(e),
);
rethrow;
}
}
// Update task - still save to local if network fails
Future<void> updateTask(Task task, {bool skipNetwork = false}) async {
final currentTask = state.tasks.firstWhere((t) => t.id == task.id, orElse: () => task);
try {
// Update UI first
state = state.copyWith(
tasks: state.tasks.map((t) => t.id == task.id ? task : t).toList(),
errorMessage: null,
);
// Save to local storage (execute regardless of network success)
await StorageUtils.updateTask(task);
// Return directly if network request is not needed
if (skipNetwork) {
return;
}
// Try to sync with network
final updatedTask = await NetworkUtils.updateTask(task);
} catch (e) {
// Keep locally updated state if network fails
state = state.copyWith(
errorMessage: NetworkUtils.getErrorMsg(e),
);
rethrow;
}
}
// Delete task
Future<void> deleteTask(String id) async {
// Save the task to be deleted for rollback on failure
final deletedTask = state.tasks.firstWhere((t) => t.id == id, orElse: () => Task(id: id, title: ''));
try {
// Update UI first
state = state.copyWith(
tasks: state.tasks.where((t) => t.id != id).toList(),
errorMessage: null,
);
// Sync to local storage
await StorageUtils.deleteTask(id);
// Try to sync with network
await NetworkUtils.deleteTask(id);
} catch (e) {
// Rollback on failure
state = state.copyWith(
tasks: [...state.tasks, deletedTask],
errorMessage: NetworkUtils.getErrorMsg(e),
);
rethrow;
}
}
}
Create theme state management (providers/theme_provider.dart):
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(),
);
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
static const _themeBox = 'settings';
static const _themeKey = 'theme_mode';
ThemeModeNotifier() : super(ThemeMode.system) {
_loadThemeMode();
}
Future<void> _loadThemeMode() async {
final box = await Hive.openBox(_themeBox);
final mode = box.get(_themeKey);
if (mode == 'light') {
state = ThemeMode.light;
} else if (mode == 'dark') {
state = ThemeMode.dark;
} else {
state = ThemeMode.system;
}
}
void setThemeMode(ThemeMode mode) {
state = mode;
final box = Hive.box(_themeBox);
if (mode == ThemeMode.light) {
box.put(_themeKey, 'light');
} else if (mode == ThemeMode.dark) {
box.put(_themeKey, 'dark');
} else {
box.put(_themeKey, 'system');
}
}
}
III. Completing Core Page Implementations
1. Home Page Implementation (pages/home_page.dart)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/task/task_item.dart';
import '../models/task_model.dart';
import 'detail_page.dart';
import 'setting_page.dart';
import '../providers/task_provider.dart';
import '../utils/network_utils.dart';
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final taskState = ref.watch(taskProvider);
final taskNotifier = ref.read(taskProvider.notifier);
// Load task data for the first time
WidgetsBinding.instance.addPostFrameCallback((_) {
if (taskState.tasks.isEmpty && taskState.isLoading) {
taskNotifier.loadTasks();
}
});
// Display error message
if (taskState.errorMessage != null && taskState.tasks.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(taskState.errorMessage!),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'Retry',
onPressed: () => taskNotifier.loadTasks(),
),
),
);
}
});
}
return Scaffold(
appBar: AppBar(
title: const Text('To-Do List'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingPage()),
),
),
],
),
body: _buildBody(context, taskState, taskNotifier),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => _navigateToDetail(context, null, taskNotifier),
),
);
}
Widget _buildBody(
BuildContext context,
TaskState taskState,
TaskNotifier taskNotifier,
) {
// Loading state
if (taskState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Empty data state
if (taskState.tasks.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('No tasks yet. Add one!'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => taskNotifier.loadTasks(),
child: const Text('Reload'),
),
],
),
);
}
// Normal task list display
return RefreshIndicator(
onRefresh: () async {
await taskNotifier.loadTasks();
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: taskState.tasks.length,
itemBuilder: (context, index) {
final task = taskState.tasks[index];
return TaskItem(
task: task,
onTap: () => _navigateToDetail(context, task, taskNotifier),
onStatusChanged: (id) {
final updatedTask = Task(
id: task.id,
title: task.title,
content: task.content,
createTime: task.createTime,
deadline: task.deadline,
isCompleted: !task.isCompleted,
);
taskNotifier.updateTask(updatedTask);
},
onDelete: (id) => taskNotifier.deleteTask(id),
);
},
),
);
}
void _navigateToDetail(
BuildContext context,
Task? task,
TaskNotifier notifier,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
task: task,
onSave: (newTask) async {
try {
if (task == null) {
// First attempt with network request
await notifier.addTask(newTask);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Task added successfully')),
);
Navigator.pop(context);
}
} else {
// First attempt with network request
await notifier.updateTask(newTask);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Task updated successfully')),
);
Navigator.pop(context);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${NetworkUtils.getErrorMsg(e)}. Will save locally.'),
action: SnackBarAction(
label: 'Confirm',
onPressed: () => _saveAgain(context, newTask, task, notifier),
),
),
);
}
}
},
),
),
);
}
// Retry saving task - save only locally, skip network request
void _saveAgain(BuildContext context, Task newTask, Task? originalTask, TaskNotifier notifier) {
if (originalTask == null) {
// Skip network request, save only locally
notifier.addTask(newTask, skipNetwork: true);
} else {
// Skip network request, save only locally
notifier.updateTask(newTask, skipNetwork: true);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Task saved locally')),
);
Navigator.pop(context);
}
}
}
2. Task List Item Component (widgets/task/task_item.dart)
import 'package:flutter/material.dart';
import '../../models/task_model.dart';
import '../../utils/date_utils.dart';
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback onTap;
final Function(String) onStatusChanged;
final Function(String) onDelete;
const TaskItem({
super.key,
required this.task,
required this.onTap,
required this.onStatusChanged,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key(task.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Deletion'),
content: const Text('Are you sure you want to delete this task?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
},
onDismissed: (direction) => onDelete(task.id),
child: Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
onTap: onTap,
leading: Checkbox(
value: task.isCompleted,
onChanged: (value) => onStatusChanged(task.id),
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted ? TextDecoration.lineThrough : null,
color: task.isCompleted ? Colors.grey : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (task.deadline != null)
Text(
'Deadline: ${DateUtils.formatDate(task.deadline!)}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
'Created: ${DateUtils.formatDate(task.createTime)}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
trailing: const Icon(Icons.arrow_forward_ios, size: 18),
),
),
);
}
}
3. Detail Page Implementation (pages/detail_page.dart)
import 'package:flutter/material.dart';
import '../models/task_model.dart';
import '../utils/date_utils.dart';
import '../utils/network_utils.dart';
class DetailPage extends StatefulWidget {
final Task? task;
final Function(Task) onSave;
const DetailPage({
super.key,
this.task,
required this.onSave,
});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
late final TextEditingController _titleController;
late final TextEditingController _contentController;
DateTime? _deadline;
bool _isCompleted = false;
bool _isSaving = false;
@override
void initState() {
super.initState();
if (widget.task != null) {
_titleController = TextEditingController(text: widget.task!.title);
_contentController = TextEditingController(text: widget.task!.content);
_deadline = widget.task!.deadline;
_isCompleted = widget.task!.isCompleted;
} else {
_titleController = TextEditingController();
_contentController = TextEditingController();
}
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
void _saveTask() async {
if (_titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a task title')),
);
return;
}
setState(() => _isSaving = true);
try {
final task = Task(
id: widget.task?.id ?? DateTime.now().microsecondsSinceEpoch.toString(),
title: _titleController.text,
content: _contentController.text,
createTime: widget.task?.createTime ?? DateTime.now(),
deadline: _deadline,
isCompleted: _isCompleted,
);
await widget.onSave(task);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(NetworkUtils.getErrorMsg(e))),
);
}
} finally {
if (mounted) {
setState(() => _isSaving = false);
}
}
}
Future<void> _selectDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _deadline ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => _deadline = picked);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.task == null ? 'Add Task' : 'Edit Task'),
actions: [
if (widget.task != null)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Deletion'),
content: const Text('Are you sure you want to delete this task?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true && context.mounted) {
Navigator.pop(context);
}
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Task Title',
border: OutlineInputBorder(),
),
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
TextField(
controller: _contentController,
decoration: const InputDecoration(
labelText: 'Task Content',
border: OutlineInputBorder(),
),
maxLines: 4,
),
const SizedBox(height: 16),
ListTile(
title: const Text('Deadline'),
subtitle: Text(_deadline != null
? DateUtils.formatDate(_deadline!)
: 'Not set'),
trailing: const Icon(Icons.calendar_today),
onTap: _selectDate,
),
SwitchListTile(
title: const Text('Completion Status'),
value: _isCompleted,
onChanged: (value) => setState(() => _isCompleted = value),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _isSaving ? null : _saveTask,
child: _isSaving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Padding(
padding: EdgeInsets.all(12),
child: Text('Save Task'),
),
),
],
),
),
);
}
}
4. Settings Page Implementation (pages/setting_page.dart)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import '../providers/theme_provider.dart';
import '../models/task_model.dart';
class SettingPage extends ConsumerWidget {
const SettingPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final themeNotifier = ref.read(themeModeProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
children: [
SwitchListTile(
title: const Text('Enable Notification Reminders'),
value: true,
onChanged: (value) {},
),
ListTile(
title: const Text('Dark Mode'),
trailing: DropdownButton<ThemeMode>(
value: themeMode,
items: const [
DropdownMenuItem(
value: ThemeMode.system,
child: Text('Follow System'),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text('Light Mode'),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text('Dark Mode'),
),
],
onChanged: (value) {
if (value != null) {
themeNotifier.setThemeMode(value);
}
},
),
),
ListTile(
title: const Text('About Us'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
showAboutDialog(
context: context,
applicationName: 'To-Do List',
applicationVersion: '1.0.0',
children: const [
Padding(
padding: EdgeInsets.only(top: 16),
child: Text('A simple and efficient task management tool to help you better plan your life and work.'),
),
],
);
},
),
ListTile(
title: const Text('Clear Cache'),
trailing: const Icon(Icons.delete),
onTap: () async {
await Hive.box<Task>('tasks').clear();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cache cleared')),
);
}
},
),
],
),
);
}
}
5. Routing Management Utility (utils/router.dart)
import 'package:flutter/material.dart';
import '../pages/home_page.dart';
import '../pages/detail_page.dart';
import '../pages/setting_page.dart';
class Router {
static const String home = '/';
static const String detail = '/detail';
static const String setting = '/setting';
static Map<String, WidgetBuilder> routes = {
home: (context) => const HomePage(),
setting: (context) => const SettingPage(),
};
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case detail:
final args = settings.arguments as Map<String, dynamic>?;
return PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) => DetailPage(
task: args?['task'],
onSave: args?['onSave'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.ease;
final tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
default:
return MaterialPageRoute(
builder: (context) => const Scaffold(
body: Center(child: Text('Page not found')),
),
);
}
}
}
6. Date Utility Class (utils/date_utils.dart)
import 'package:intl/intl.dart';
class DateUtils {
static String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
static String formatDateTime(DateTime date) {
return DateFormat('yyyy-MM-dd HH:mm').format(date);
}
static String formatFriendly(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final dateOnly = DateTime(date.year, date.month, date.day);
if (dateOnly == today) {
return 'Today ${DateFormat('HH:mm').format(date)}';
} else if (dateOnly == yesterday) {
return 'Yesterday ${DateFormat('HH:mm').format(date)}';
} else if (date.year == now.year) {
return DateFormat('MM-dd HH:mm').format(date);
} else {
return DateFormat('yyyy-MM-dd HH:mm').format(date);
}
}
}
IV. Summary and Extensions
Through this lesson, we've completed the full development process of the to-do list app, implementing the following features:
-
Data Persistence Solution:
- Integrated Dio for network requests, supporting CRUD operations for tasks
- Used Hive for local data storage, ensuring offline availability
- Implemented network and local data synchronization, with automatic local saving when network fails
-
State Management System:
- Implemented reactive state management with Riverpod
- Designed a comprehensive task state model including loading status and error messages
- Achieved atomicity of data operations, ensuring rollback capability or local data retention on failure
-
User Experience Optimization:
- Comprehensive loading states and error handling
- Local saving option when network requests fail
- Clear operation feedback to keep users informed of current status
- Support for dark mode switching to adapt to different usage scenarios
-
Interaction Design:
- Swipe-to-delete functionality for list items
- Page transition animations
- Real-time form operation feedback
- Pull-to-refresh for data updates
Top comments (0)