The monitoring feature follows Clean Architecture principles with a clear separation between domain interfaces and data implementations. This page documents the repository layer that handles data operations for inspection questions.
Architecture Overview
The repository pattern provides an abstraction layer between the domain and data layers:
Domain Layer : QuestionRepository interface defines contracts
Data Layer : QuestionRepositoryImpl implements the interface
Data Source : QuestionRemoteDataSource handles HTTP communication
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ ( QuestionsController ) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Domain Layer │
│ ( QuestionRepository ) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Data Layer │
│ ( QuestionRepositoryImpl ) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Data Source │
│ ( QuestionRemoteDataSource ) │
└─────────────────────────────────────────┘
QuestionRepository (Interface)
The abstract repository interface defines all operations for managing inspection questions.
Location: lib/feature/monitoring/domain/repositories/question_repository.dart
Methods
getPreguntas
Future<Either<Failure, List<Pregunta>>>
Retrieves all questions for a specific apiary. The ID of the apiary to fetch questions for
Returns Right(List<Pregunta>) on success, or Left(Failure) on error.
Example: final result = await repository. getPreguntas ( 'apiary123' );
result. fold (
(failure) => print ( 'Error: ${ failure . message } ' ),
(questions) => print ( 'Found ${ questions . length } questions' ),
);
createPregunta
Future<Either<Failure, Pregunta>>
Creates a new inspection question. The question to create. The id field can be empty.
Returns Right(Pregunta) with the created question (including server-assigned ID), or Left(Failure) on error.
Example: final newQuestion = Pregunta (
id : '' ,
apiarioId : 'apiary123' ,
texto : '¿Estado de la colmena?' ,
tipoRespuesta : 'texto' ,
obligatoria : true ,
orden : 1 ,
);
final result = await repository. createPregunta (newQuestion);
result. fold (
(failure) => print ( 'Creation failed: ${ failure . message } ' ),
(created) => print ( 'Created with ID: ${ created . id } ' ),
);
updatePregunta
Future<Either<Failure, Pregunta>>
Updates an existing inspection question. The question with updated values. Must have a valid id.
Returns Right(Pregunta) with the updated question, or Left(Failure) on error.
Example: final updated = existingQuestion. copyWith (
texto : 'Pregunta actualizada' ,
obligatoria : false ,
);
final result = await repository. updatePregunta (updated);
deletePregunta
Future<Either<Failure, void>>
Permanently deletes an inspection question. The ID of the question to delete
Returns Right(null) on success, or Left(Failure) on error.
Example: final result = await repository. deletePregunta ( 'question123' );
result. fold (
(failure) => showError ( 'No se pudo eliminar' ),
(_) => showSuccess ( 'Pregunta eliminada' ),
);
reorderPreguntas
Future<Either<Failure, void>>
Updates the display order of multiple questions. The ID of the apiary whose questions are being reordered
Ordered list of question IDs
Example: final newOrder = [ 'q3' , 'q1' , 'q2' ]; // Reorder questions
final result = await repository. reorderPreguntas ( 'apiary123' , newOrder);
loadDefaults
Future<Either<Failure, void>>
Loads default questions into an apiary from the question bank. The ID of the apiary to load defaults into
Example: final result = await repository. loadDefaults ( 'apiary123' );
result. fold (
(failure) => print ( 'Failed to load defaults' ),
(_) => print ( 'Default questions loaded successfully' ),
);
getTemplates
Future<Either<Failure, List<Pregunta>>>
Retrieves available question templates from the global question bank. Returns Right(List<Pregunta>) with template questions, or Left(Failure) on error.
Example: final result = await repository. getTemplates ();
result. fold (
(failure) => print ( 'Failed to load templates' ),
(templates) => print ( 'Found ${ templates . length } templates' ),
);
QuestionRepositoryImpl (Implementation)
The concrete implementation of the repository interface that handles authentication and delegates to the remote data source.
Location: lib/feature/monitoring/data/repositories/question_repository_impl.dart
Dependencies
remoteDataSource
QuestionRemoteDataSource
required
The data source that handles HTTP requests
localDataSource
AuthLocalDataSource
required
The local data source for retrieving authentication tokens
Constructor
final repository = QuestionRepositoryImpl (
remoteDataSource : questionRemoteDataSource,
localDataSource : authLocalDataSource,
);
Implementation Details
Each method follows this pattern:
Retrieve authentication token from local storage
Validate token exists
Call remote data source with token
Handle exceptions and map to failures
Return Either result
Example Implementation:
@override
Future < Either < Failure , List < Pregunta >>> getPreguntas ( String apiaryId) async {
try {
// Step 1 & 2: Get and validate token
final token = await localDataSource. getToken ();
if (token == null ) return const Left ( AuthFailure ( 'No token found' ));
// Step 3: Call data source
final result = await remoteDataSource. getPreguntas (apiaryId, token);
// Step 5: Return success
return Right (result);
} catch (e) {
// Step 4: Handle errors
return Left ( ServerFailure (e. toString ()));
}
}
Error Handling
The implementation handles two main error types:
Returned when no authentication token is available in local storage. Resolution: User needs to log in again.
Returned when any exception occurs during the operation (network errors, API errors, etc.). Message: Contains the exception message for debugging.
QuestionRemoteDataSource
Handles direct HTTP communication with the backend API.
Location: lib/feature/monitoring/data/datasources/question_remote_datasource.dart
API Endpoints
GET /api/v1/questions/apiary/:apiaryId
Fetches all questions for an apiary Headers: Authorization: Bearer {token}Response: Array of question objects
Creates a new question Headers: Authorization: Bearer {token}Body: Question object (JSON)Response: Created question with ID
PUT /api/v1/questions/:id
Updates an existing question Headers: Authorization: Bearer {token}Body: Updated question object (JSON)Response: Updated question
DELETE /api/v1/questions/:id
Deletes a question Headers: Authorization: Bearer {token}Response: No content (204)
PUT /api/v1/questions/apiary/:apiaryId/reorder
Reorders questions Headers: Authorization: Bearer {token}Body: { "order": ["id1", "id2", "id3"] }Response: No content (204)
POST /api/v1/questions/load_defaults/:apiaryId
Loads default questions Headers: Authorization: Bearer {token}Response: No content (204)
GET /api/v1/questions/templates
Fetches question templates Headers: Authorization: Bearer {token}Response: Array of template questions
Implementation Example
@override
Future < List < Pregunta >> getPreguntas ( String apiaryId, String token) async {
try {
final response = await httpClient. get (
'/api/v1/questions/apiary/ $ apiaryId ' ,
options : Options (headers : { 'Authorization' : 'Bearer $ token ' }),
);
final List < dynamic > data = response.data as List < dynamic >;
return data
. map < Pregunta >(
(json) => Pregunta . fromJson ( Map < String , dynamic >. from (json)),
)
. toList ();
} on DioException catch (e) {
throw Exception (
e.response ? .data[ 'message' ] ?? 'Error obteniendo preguntas' ,
);
}
}
Dependency Injection
The repository and its dependencies are typically registered with a dependency injection container (e.g., Riverpod providers).
import 'package:flutter_riverpod/flutter_riverpod.dart' ;
import 'package:dio/dio.dart' ;
// Data source providers
final dioProvider = Provider < Dio >((ref) => Dio ( BaseOptions (
baseUrl : 'https://api.softbee.app' ,
)));
final questionRemoteDataSourceProvider = Provider < QuestionRemoteDataSource >(
(ref) => QuestionRemoteDataSourceImpl (ref. watch (dioProvider)),
);
// Repository provider
final questionRepositoryProvider = Provider < QuestionRepository >(
(ref) => QuestionRepositoryImpl (
remoteDataSource : ref. watch (questionRemoteDataSourceProvider),
localDataSource : ref. watch (authLocalDataSourceProvider),
),
);
// Controller provider
final questionsProvider = StateNotifierProvider < QuestionsController , QuestionsState >(
(ref) => QuestionsController (ref. watch (questionRepositoryProvider)),
);
Testing
Mocking the Repository
import 'package:mocktail/mocktail.dart' ;
class MockQuestionRepository extends Mock implements QuestionRepository {}
void main () {
late MockQuestionRepository mockRepository;
late QuestionsController controller;
setUp (() {
mockRepository = MockQuestionRepository ();
controller = QuestionsController (mockRepository);
});
test ( 'fetchPreguntas loads questions successfully' , () async {
// Arrange
final questions = [
Pregunta (
id : '1' ,
apiarioId : 'apiary123' ,
texto : 'Test question' ,
tipoRespuesta : 'texto' ,
obligatoria : true ,
orden : 1 ,
),
];
when (() => mockRepository. getPreguntas ( 'apiary123' ))
. thenAnswer ((_) async => Right (questions));
// Act
await controller. fetchPreguntas ( 'apiary123' );
// Assert
expect (controller.state.preguntas, questions);
expect (controller.state.isLoading, false );
expect (controller.state.error, null );
});
test ( 'fetchPreguntas handles errors' , () async {
// Arrange
when (() => mockRepository. getPreguntas ( 'apiary123' ))
. thenAnswer ((_) async => Left ( ServerFailure ( 'Network error' )));
// Act
await controller. fetchPreguntas ( 'apiary123' );
// Assert
expect (controller.state.error, 'Network error' );
expect (controller.state.isLoading, false );
});
}
Best Practices
Interface Programming : Always program against the QuestionRepository interface, not the implementation. This enables easy testing and future changes.
Token Management : The repository handles token retrieval automatically. Never pass tokens directly from the presentation layer.
Error Messages : Server failures include the raw exception message. Consider mapping these to user-friendly messages in the presentation layer.
Template Mapping : The getTemplates method handles multiple response formats from the API (both Spanish and English field names) for maximum compatibility.