【Flutter Todo 实战】第五章:数据模型详解

· 5 min read

5.1 什么是数据模型?

数据模型(Model) 是用来描述应用数据的类。它定义了:

  • 数据有哪些属性(字段)
  • 每个属性的类型
  • 如何创建、修改、转换数据

类比理解

想象你要设计一个数据库表来存储任务信息:

1CREATE TABLE tasks (
2    id TEXT PRIMARY KEY,
3    title TEXT NOT NULL,
4    is_completed BOOLEAN DEFAULT FALSE,
5    created_at DATETIME,
6    due_date DATETIME
7);

在 Flutter 中,我们用 Dart 类来实现同样的功能:

class Task {
  final String id;
  final String title;
  final bool isCompleted;
  final DateTime createdAt;
  final DateTime? dueDate;
}

5.2 Task 类的完整代码

/// Task Model
/// Represents a single todo item with its properties
class Task {
  /// Unique identifier for the task
  final String id;
  
  /// Title/description of the task
  final String title;
  
  /// Whether the task is completed
  final bool isCompleted;
  
  /// Timestamp when the task was created
  final DateTime createdAt;
  
  /// Optional due date for the task
  final DateTime? dueDate;

  Task({
    required this.id,
    required this.title,
    this.isCompleted = false,
    required this.createdAt,
    this.dueDate,
  });

  /// Create a copy of this task with modified fields
  Task copyWith({
    String? id,
    String? title,
    bool? isCompleted,
    DateTime? createdAt,
    DateTime? dueDate,
  }) {
    return Task(
      id: id ?? this.id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt ?? this.createdAt,
      dueDate: dueDate ?? this.dueDate,
    );
  }

  /// Convert Task to JSON for local storage
  Map toJson() {
    return {
      'id': id,
      'title': title,
      'isCompleted': isCompleted,
      'createdAt': createdAt.toIso8601String(),
      'dueDate': dueDate?.toIso8601String(),
    };
  }

  /// Create Task from JSON (from local storage)
  factory Task.fromJson(Map json) {
    return Task(
      id: json['id'] as String,
      title: json['title'] as String,
      isCompleted: json['isCompleted'] as bool,
      createdAt: DateTime.parse(json['createdAt'] as String),
      dueDate: json['dueDate'] != null 
          ? DateTime.parse(json['dueDate'] as String) 
          : null,
    );
  }

  @override
  String toString() {
    return 'Task(id: $id, title: $title, isCompleted: $isCompleted)';
  }
}

/// Filter types for task list
enum TaskFilter {
  all('全部'),
  active('进行中'),
  completed('已完成');

  final String label;
  const TaskFilter(this.label);
}

5.3 逐行解析

类定义和属性

class Task {
  final String id;           // 任务唯一标识
  final String title;        // 任务标题
  final bool isCompleted;    // 是否已完成
  final DateTime createdAt;  // 创建时间
  final DateTime? dueDate;   // 截止日期(可为空)

关键字解释:

关键字 含义 说明
final 不可变 赋值后不能修改
String 字符串类型 存储文本
bool 布尔类型 true/false
DateTime 日期时间类型 存储时间
DateTime? 可空的日期时间 ? 表示可为 null

构造函数

Task({
  required this.id,
  required this.title,
  this.isCompleted = false,
  required this.createdAt,
  this.dueDate,
});

语法解析:

  • {} 包裹的参数是命名参数,调用时必须指定参数名
  • required 表示该参数必须提供
  • this.isCompleted = false 表示有默认值 false
  • this.dueDate 没有 required 也没有默认值,表示可选(可为 null)

使用示例:

// 创建一个新任务
final task = Task(
  id: 'abc-123',
  title: '买牛奶',
  createdAt: DateTime.now(),
  dueDate: DateTime.now().add(Duration(days: 1)),
);

// isCompleted 使用默认值 false

copyWith 方法

Task copyWith({
  String? id,
  String? title,
  bool? isCompleted,
  DateTime? createdAt,
  DateTime? dueDate,
}) {
  return Task(
    id: id ?? this.id,
    title: title ?? this.title,
    isCompleted: isCompleted ?? this.isCompleted,
    createdAt: createdAt ?? this.createdAt,
    dueDate: dueDate ?? this.dueDate,
  );
}

为什么需要 copyWith?

因为 final 字段创建后不能修改,所以要用 copyWith 创建一个新的 Task,只改变需要的字段。

使用示例:

final task = Task(
  id: 'abc-123',
  title: '买牛奶',
  createdAt: DateTime.now(),
);

// 标记为已完成(创建新对象,不修改原对象)
final completedTask = task.copyWith(isCompleted: true);

print(task.isCompleted);        // false(原对象不变)
print(completedTask.isCompleted); // true(新对象已改变)

?? 运算符:

id ?? this.id

表示:如果 id 不为 null,使用 id;否则使用 this.id

toJson 方法

Map toJson() {
  return {
    'id': id,
    'title': title,
    'isCompleted': isCompleted,
    'createdAt': createdAt.toIso8601String(),
    'dueDate': dueDate?.toIso8601String(),
  };
}

作用: 将 Task 对象转换为 JSON 格式,用于本地存储。

返回类型: Map 是一个键值对映射:

  • String 是键的类型(属性名)
  • dynamic 是值的类型(可以是任意类型)

DateTime.toIso8601String():

将日期时间转换为标准格式的字符串,例如:

2024-01-15T10:30:00.000

?. 运算符:

dueDate?.toIso8601String()

表示:如果 dueDate 不为 null,调用 toIso8601String();否则返回 null。

fromJson 工厂方法

factory Task.fromJson(Map json) {
  return Task(
    id: json['id'] as String,
    title: json['title'] as String,
    isCompleted: json['isCompleted'] as bool,
    createdAt: DateTime.parse(json['createdAt'] as String),
    dueDate: json['dueDate'] != null 
        ? DateTime.parse(json['dueDate'] as String) 
        : null,
  );
}

作用: 从 JSON 数据创建 Task 对象,用于从本地存储读取数据。

关键字解释:

  • factory - 工厂构造函数,可以返回现有实例或子类
  • as String - 类型转换,将 dynamic 转换为 String
  • DateTime.parse() - 将字符串解析为 DateTime 对象

使用示例:

// 假设从存储读取的 JSON 数据
final json = {
  'id': 'abc-123',
  'title': '买牛奶',
  'isCompleted': false,
  'createdAt': '2024-01-15T10:30:00.000',
  'dueDate': null,
};

// 转换为 Task 对象
final task = Task.fromJson(json);

toString 方法

@override
String toString() {
  return 'Task(id: $id, title: $title, isCompleted: $isCompleted)';
}

作用: 定义对象转换为字符串的格式,方便调试打印。

使用示例:

final task = Task(id: '123', title: '测试', createdAt: DateTime.now());
print(task);  // 输出: Task(id: 123, title: 测试, isCompleted: false)

5.4 TaskFilter 枚举

enum TaskFilter {
  all('全部'),
  active('进行中'),
  completed('已完成');

  final String label;
  const TaskFilter(this.label);
}

什么是枚举?

枚举(Enum)是一种定义固定值集合的方式。

使用示例:

// 获取当前筛选条件
TaskFilter filter = TaskFilter.all;

// 获取显示标签
print(filter.label);  // 输出: 全部

// 比较
if (filter == TaskFilter.completed) {
  print('显示已完成任务');
}

// 遍历所有值
for (var filter in TaskFilter.values) {
  print(filter.label);
}

5.5 完整使用示例

void main() {
  // 1. 创建任务
  final task1 = Task(
    id: 'task-001',
    title: '学习 Flutter',
    createdAt: DateTime.now(),
    dueDate: DateTime.now().add(Duration(days: 7)),
  );
  
  final task2 = Task(
    id: 'task-002',
    title: '买牛奶',
    createdAt: DateTime.now(),
  );
  
  print('创建的任务:');
  print(task1);
  print(task2);
  
  // 2. 修改任务(使用 copyWith)
  final completedTask = task1.copyWith(isCompleted: true);
  print('\n标记为完成:');
  print(completedTask);
  
  // 3. 转换为 JSON(保存到本地)
  final json = task1.toJson();
  print('\nJSON 格式:');
  print(json);
  
  // 4. 从 JSON 恢复
  final restoredTask = Task.fromJson(json);
  print('\n恢复的任务:');
  print(restoredTask);
  
  // 5. 使用筛选枚举
  TaskFilter currentFilter = TaskFilter.active;
  print('\n当前筛选: ${currentFilter.label}');
}

输出:

创建的任务:
Task(id: task-001, title: 学习 Flutter, isCompleted: false)
Task(id: task-002, title: 买牛奶, isCompleted: false)

标记为完成:
Task(id: task-001, title: 学习 Flutter, isCompleted: true)

JSON 格式:
{id: task-001, title: 学习 Flutter, isCompleted: false, createdAt: 2024-01-15T10:30:00.000, dueDate: 2024-01-22T10:30:00.000}

恢复的任务:
Task(id: task-001, title: 学习 Flutter, isCompleted: false)

当前筛选: 进行中

5.6 小结

概念 作用
final 定义不可变属性
required 标记必须传入的参数
? 标记可为 null 的类型
copyWith 创建修改后的副本
toJson 转换为 JSON 格式
fromJson 从 JSON 创建对象
enum 定义固定值集合

下一步第六章:状态管理详解

Related Articles