基于 分离 + CodeGen 映射 的最佳实践开发流程,涵盖从领域建模到 DTO、映射、仓库实现、UseCase、ViewModel、UI,再到各层单元/集成测试和 CI 集成。按此流程,既能保持清晰分层,又能用工具最大化减少样板、提高维护效率。

一、领域层(Domain)

定义实体(Entity)和值对象(Value Object)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lib/features/members/domain/entities/member_entity.dart
class MemberEntity {
final String name;
final String accountId;
final String accountRole;
final int accountStatus;

MemberEntity({
required this.name,
required this.accountId,
required this.accountRole,
required this.accountStatus,
});

bool get isActive => accountStatus == 1;
}

定义领域请求/响应模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lib/features/members/domain/entities/member_update_item.dart
class MemberUpdateItem {
final int? accountStatus;
final int? resetPassword;
MemberUpdateItem({this.accountStatus, this.resetPassword});
}

// lib/features/members/domain/entities/member_creation_request.dart
class ManagerCreationInfo {
final String accountId, name;
/*…*/
}
class SupportCreationInfo {
final int createNum;
/*…*/
}

定义仓库接口

1
2
3
4
5
6
7
8
9
10
// lib/features/members/domain/repositories/member_repository.dart
abstract class MemberRepository {
Future<List<MemberEntity>> fetchTeamMembers(int teamId);
Future<bool> updateMemberStatus(String accountId, MemberUpdateItem item);
Future<List<MemberEntity>> createTeamMembers({
required int teamId,
ManagerCreationInfo? manager,
SupportCreationInfo? support,
});
}

二、数据层(Data / Infrastructure)

1. 定义 DTO(只做 JSON 序列化)

lib/features/members/data/dto/ 下,用 @JsonSerializable(fieldRename: FieldRename.snake)

1
2
3
4
5
6
7
8
9
10
11
// member_dto.dart
@JsonSerializable(fieldRename: FieldRename.snake)
class MemberDto {
final String name;
final String accountId;
final String accountRole;
final int accountStatus;

factory MemberDto.fromJson(Map<String, dynamic> json) => _$MemberDtoFromJson(json);
Map<String, dynamic> toJson() => _$MemberDtoToJson(this);
}

同理定义 MemberUpdateItemDto, ManagerCreationInfoDto, SupportCreationInfoDto, MemberCreationResponseDto 等。

2. 生成序列化代码

1
flutter pub run build_runner build --delete-conflicting-outputs

3. 写 Mapper Extension(DTO → Entity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// lib/features/members/data/mappers/member_mapper.dart
extension MemberDtoX on MemberDto {
MemberEntity toEntity() => MemberEntity(
name: name,
accountId: accountId,
accountRole: accountRole,
accountStatus: accountStatus,
);
}

extension MemberCreationResponseDtoX on MemberCreationResponseDto {
List<MemberEntity> toMemberEntities() {
final out = <MemberEntity>[];
if (manager != null) out.add(manager!.toMemberEntity());
if (support != null) out.addAll(support!.toMemberEntities());
return out;
}
}

三、仓库实现(RepositoryImpl)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// lib/features/members/data/repositories/member_repository_impl.dart
class MemberRepositoryImpl implements MemberRepository {
final MCRequestManager _api;
MemberRepositoryImpl(this._api);

@override
Future<List<MemberEntity>> fetchTeamMembers(int teamId) async {
final res = await _api.getTeamMemberListAsync(teamId);
final dtos = (res['members'] as List)
.map((e) => MemberDto.fromJson(e)).toList();
return dtos.map((d) => d.toEntity()).toList();
}

@override
Future<bool> updateMemberStatus(String accountId, MemberUpdateItem item) async {
final dto = MemberUpdateItemDto(
accountStatus: item.accountStatus, resetPassword: item.resetPassword);
await _api.updateTeamMemberAsync(accountId, dto.toJson());
return true;
}

@override
Future<List<MemberEntity>> createTeamMembers({
required int teamId,
ManagerCreationInfo? manager,
SupportCreationInfo? support,
}) async {
final managerDto = manager != null
? ManagerCreationInfoDto(accountId: manager.accountId, name: manager.name)
: null;
final supportDto = support != null
? SupportCreationInfoDto(createNum: support.createNum)
: null;

final data = await _api.createTeamMemberAsync(
teamId,
manager: managerDto?.toJson(),
support: supportDto?.toJson(),
);
final respDto = MemberCreationResponseDto.fromJson(data);
return respDto.toMemberEntities();
}
}

四、应用层(Application)

为每个业务场景写 UseCase。

1. LoadMembersUseCase

1
2
3
4
5
6
7
class LoadMembersUseCase {
final MemberRepository _repo;
LoadMembersUseCase(this._repo);

Future<List<MemberEntity>> execute(int teamId) =>
_repo.fetchTeamMembers(teamId);
}

2. UpdateMemberStatusUseCase

1
2
3
4
5
6
7
class UpdateMemberStatusUseCase {
final MemberRepository _repo;
UpdateMemberStatusUseCase(this._repo);

Future<bool> execute(String accountId, int newStatus) =>
_repo.updateMemberStatus(accountId, MemberUpdateItem(accountStatus: newStatus));
}

3. CreateMembersUseCase

1
2
3
4
5
6
7
8
9
10
class CreateMembersUseCase {
final MemberRepository _repo;
CreateMembersUseCase(this._repo);

Future<List<MemberEntity>> execute({
required int teamId,
ManagerCreationInfo? manager,
SupportCreationInfo? support,
}) => _repo.createTeamMembers(teamId: teamId, manager: manager, support: support);
}

五、表现层(Presentation)

1. State + ViewModel

定义状态类和 StateNotifier,注入 UseCase。

1
2
3
4
5
6
7
8
9
10
11
class MembersState { /* status, list, error */ }

class MembersViewModel extends StateNotifier<MembersState> {
final LoadMembersUseCase _load;
final UpdateMemberStatusUseCase _update;
final CreateMembersUseCase _create;
MembersViewModel(this._load, this._update, this._create) : super(MembersState());
Future<void> load(int teamId) async { /*…*/ }
Future<void> update(String id, int status) async { /*…*/ }
Future<void> create({…}) async { /*…*/ }
}

2. Provider 注册

1
2
3
4
5
6
final loadUseCaseProvider = Provider((ref) => LoadMembersUseCase(ref.read(repo)));
final viewModelProvider = StateNotifierProvider.family<MembersViewModel, MembersState, int>((ref, teamId) => MembersViewModel(
ref.read(loadUseCaseProvider),
ref.read(updateUseCaseProvider),
ref.read(createUseCaseProvider),
)..load(teamId));

3. Widget 调用

1
2
3
4
5
6
7
class MembersPage extends ConsumerWidget {
Widget build(...) {
final state = ref.watch(viewModelProvider(teamId));
ref.listen(viewModelProvider(teamId), (_, s) { if (s.isError) showError(...); });
return state.isLoading ? Loading() : CustomTable(items: state.list, ...);
}
}

六、测试策略

1. 单元测试(UseCase/Mapper/ViewModel)

  • UseCase:用 Mockito 生成 MockMemberRepository,测试 execute 成功/失败分支。
  • Mapper:给一个 DTO 测 dto.toEntity() 输出符合预期。
  • ViewModel:用 ProviderContainer override UseCase Provider,驱动 state 变化测试。

2. 集成测试(RepositoryImpl)

在专门的测试环境上跑 MemberRepositoryImpl.fetchTeamMembers 等方法,验证真实 API、JSON 解析、映射流程。

3. CI 集成

  • 阶段 1:CI 每次提交 flutter test --coverage,跑单元测试。
  • 阶段 2:定时或手动触发少量集成测试,保证后端兼容。
  • 阶段 3:可选 E2E/UI 测试,保证整个用户流畅。

七、工具 & 脚本

  • CodeGenbuild_runner + json_serializable +(可选)dart_mappable
  • Mockmockito + build_runner
  • Lint & CI:确保 build_runner build 无报错,所有测试覆盖率可观。