DEV Community

Ge Ji
Ge Ji

Posted on

Flutter Lesson 17: Project Practice: Comprehensive Application Development (Part 2)

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
Enter fullscreen mode Exit fullscreen mode

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.';
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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'),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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')),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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')),
          ),
        );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

IV. Summary and Extensions

Through this lesson, we've completed the full development process of the to-do list app, implementing the following features:

  1. 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
  2. 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
  3. 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
  4. 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)