Data Layer
The bridge between your app and the outside world
Open interactive version (quiz + challenge)Real-world analogy
What is it?
The Data Layer implements the repository interfaces defined by the domain layer. It contains DTOs (Data Transfer Objects) for JSON serialization, remote data sources for API communication via Dio, local data sources for caching via Hive, and repository implementations that orchestrate between remote and local sources. The data layer is the only layer that knows about HTTP, JSON, databases, and external services.
Real-world relevance
In team_mvp_kit, the data layer handles the messy reality of network communication. When the API returns snake_case JSON, the DTO converts it to camelCase entities. When the network is down, the repository falls back to cached data. When the auth token expires, the Dio interceptor refreshes it automatically. The presentation and domain layers never see any of this complexity -- they just get clean entities and typed failures.
Key points
- DTOs with json_serializable — Data Transfer Objects mirror the API response structure. Using json_serializable, Dart generates fromJson and toJson boilerplate for you.
- DTO to Entity Conversion — DTOs include a toEntity method that converts API data to domain entities. This is where format differences are resolved -- snake_case to camelCase, strings to enums, timestamps to DateTime.
- Entity to DTO Conversion — When sending data to the API, you need the reverse conversion. A fromEntity factory converts domain entities back to DTOs for serialization.
- Remote Data Source with Dio — The remote data source handles all API communication using Dio. It knows about endpoints, HTTP methods, headers, and response parsing.
- Local Data Source with Hive — The local data source handles caching and offline storage. In team_mvp_kit, Hive is used for fast key-value storage on the device.
- Repository Implementation — The repository implementation ties everything together. It decides when to call the remote source, when to use cached data, and converts DTOs to entities.
- API Client Setup in team_mvp_kit — team_mvp_kit configures Dio with base URL, interceptors for auth tokens, logging, and error transformation in the infrastructure layer.
- Error Mapping in Repository — The repository converts low-level exceptions (DioException, SocketException) into domain Failure types that the use case and BLoC understand.
- Running Code Generation — After creating or modifying DTOs with json_serializable annotations, you run build_runner to generate the fromJson/toJson implementations.
Code example
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
// ---- DTO ----
class TaskDto {
final String id;
final String title;
final String description;
final String priority;
final String status;
final String createdAt;
final String? dueDate;
final String assigneeId;
const TaskDto({
required this.id,
required this.title,
required this.description,
required this.priority,
required this.status,
required this.createdAt,
this.dueDate,
required this.assigneeId,
});
factory TaskDto.fromJson(Map<String, dynamic> json) {
return TaskDto(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
priority: json['priority'] as String,
status: json['status'] as String,
createdAt: json['created_at'] as String,
dueDate: json['due_date'] as String?,
assigneeId: json['assignee_id'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'priority': priority,
'status': status,
'created_at': createdAt,
'due_date': dueDate,
'assignee_id': assigneeId,
};
}
Task toEntity() {
return Task(
id: id,
title: title,
description: description,
priority: TaskPriority.values.firstWhere(
(e) => e.name == priority,
orElse: () => TaskPriority.medium,
),
status: TaskStatus.values.firstWhere(
(e) => e.name == status,
orElse: () => TaskStatus.todo,
),
createdAt: DateTime.parse(createdAt),
dueDate: dueDate != null
? DateTime.parse(dueDate!)
: null,
assigneeId: assigneeId,
);
}
static TaskDto fromEntity(Task task) {
return TaskDto(
id: task.id,
title: task.title,
description: task.description,
priority: task.priority.name,
status: task.status.name,
createdAt: task.createdAt.toIso8601String(),
dueDate: task.dueDate?.toIso8601String(),
assigneeId: task.assigneeId,
);
}
}
// ---- Remote Data Source ----
abstract class TaskRemoteDataSource {
Future<List<TaskDto>> getTasks();
Future<TaskDto> getTaskById(String id);
Future<TaskDto> createTask(TaskDto dto);
Future<TaskDto> updateTask(TaskDto dto);
Future<void> deleteTask(String id);
}
class TaskRemoteDataSourceImpl
implements TaskRemoteDataSource {
final Dio _dio;
const TaskRemoteDataSourceImpl(this._dio);
@override
Future<List<TaskDto>> getTasks() async {
final response = await _dio.get('/api/tasks');
final list = response.data['data'] as List;
return list
.map((json) =>
TaskDto.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<TaskDto> getTaskById(String id) async {
final response = await _dio.get('/api/tasks/$id');
return TaskDto.fromJson(
response.data['data'] as Map<String, dynamic>);
}
@override
Future<TaskDto> createTask(TaskDto dto) async {
final response = await _dio.post(
'/api/tasks',
data: dto.toJson(),
);
return TaskDto.fromJson(
response.data['data'] as Map<String, dynamic>);
}
@override
Future<TaskDto> updateTask(TaskDto dto) async {
final response = await _dio.put(
'/api/tasks/${dto.id}',
data: dto.toJson(),
);
return TaskDto.fromJson(
response.data['data'] as Map<String, dynamic>);
}
@override
Future<void> deleteTask(String id) async {
await _dio.delete('/api/tasks/$id');
}
}
// ---- Repository Implementation ----
class TaskRepositoryImpl implements TaskRepository {
final TaskRemoteDataSource _remote;
const TaskRepositoryImpl(this._remote);
@override
Future<List<Task>> getTasks() async {
final dtos = await _remote.getTasks();
return dtos.map((dto) => dto.toEntity()).toList();
}
@override
Future<Task> getTaskById(String id) async {
final dto = await _remote.getTaskById(id);
return dto.toEntity();
}
@override
Future<void> createTask(Task task) async {
final dto = TaskDto.fromEntity(task);
await _remote.createTask(dto);
}
@override
Future<void> updateTask(Task task) async {
final dto = TaskDto.fromEntity(task);
await _remote.updateTask(dto);
}
@override
Future<void> deleteTask(String id) async {
await _remote.deleteTask(id);
}
}Line-by-line walkthrough
- 1. TaskDto mirrors the API response with String fields for all values including dates and enums.
- 2. fromJson factory reads each field from the JSON map, handling nullable fields like dueDate.
- 3. toJson creates a Map with snake_case keys matching the API's expected format.
- 4. toEntity converts strings to proper Dart types: DateTime.parse for dates, enum.values.firstWhere for enums.
- 5. firstWhere with orElse provides a safe default if the API returns an unexpected enum value.
- 6. fromEntity is the reverse: entity fields converted back to strings for JSON serialization.
- 7. TaskRemoteDataSource defines the abstract contract for all API operations.
- 8. TaskRemoteDataSourceImpl takes a Dio instance and implements each method with HTTP calls.
- 9. getTasks sends a GET request and maps the response JSON list to a list of TaskDtos.
- 10. createTask sends a POST with the DTO's JSON body and returns the server's response as a new DTO.
- 11. updateTask uses PUT with the task ID in the URL path.
- 12. deleteTask sends a DELETE request with just the ID.
- 13. TaskRepositoryImpl implements the domain's TaskRepository interface.
- 14. getTasks fetches DTOs from remote, converts each to an entity with toEntity.
- 15. createTask and updateTask convert entities to DTOs before sending to the remote source.
Spot the bug
class UserRepositoryImpl implements UserRepository {
final Dio _dio;
UserRepositoryImpl(this._dio);
@override
Future<User> getUser(String id) async {
final response = await _dio.get('/api/users/$id');
final json = response.data;
return User(
id: json['id'],
name: json['first_name'] + ' ' + json['last_name'],
email: json['email'],
);
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- json_serializable Package (pub.dev)
- Dio HTTP Client (pub.dev)
- Hive Database (pub.dev)