18 KiB
Review API Integration - Implementation Summary
Overview
Successfully integrated the Review/Feedback API into the Flutter Worker app, replacing mock review data with real API calls from the Frappe/ERPNext backend.
Implementation Date
November 17, 2024
API Endpoints Integrated
1. Get List Reviews
POST /api/method/building_material.building_material.api.item_feedback.get_list
Request Body:
{
"limit_page_length": 10,
"limit_start": 0,
"item_id": "GIB20 G04"
}
2. Create/Update Review
POST /api/method/building_material.building_material.api.item_feedback.update
Request Body:
{
"item_id": "Gạch ốp Signature SIG.P-8806",
"rating": 0.5, // 0-1 scale (0.5 = 2.5 stars out of 5)
"comment": "Good job 2",
"name": "ITEM-{item_id}-{user_email}" // Optional for updates
}
3. Delete Review
POST /api/method/building_material.building_material.api.item_feedback.delete
Request Body:
{
"name": "ITEM-{item_id}-{user_email}"
}
Rating Scale Conversion
CRITICAL: The API uses a 0-1 rating scale while the UI uses 1-5 stars.
Conversion Formulas
- API to UI:
stars = (apiRating * 5).round() - UI to API:
apiRating = stars / 5.0
Examples
| API Rating | Stars (Decimal) | Stars (Rounded) |
|---|---|---|
| 0.2 | 1.0 | 1 star |
| 0.4 | 2.0 | 2 stars |
| 0.5 | 2.5 | 3 stars |
| 0.8 | 4.0 | 4 stars |
| 1.0 | 5.0 | 5 stars |
Implementation:
Review.starsRatinggetter: Returns rounded integer (0-5)Review.starsRatingDecimalgetter: Returns exact decimal (0-5)starsToApiRating()helper: Converts UI stars to API ratingapiRatingToStars()helper: Converts API rating to UI stars
File Structure Created
lib/features/reviews/
data/
datasources/
reviews_remote_datasource.dart # API calls with Dio
models/
review_model.dart # JSON serialization
repositories/
reviews_repository_impl.dart # Repository implementation
domain/
entities/
review.dart # Domain entity
repositories/
reviews_repository.dart # Repository interface
usecases/
get_product_reviews.dart # Fetch reviews use case
submit_review.dart # Submit review use case
delete_review.dart # Delete review use case
presentation/
providers/
reviews_provider.dart # Riverpod providers
reviews_provider.g.dart # Generated provider code (manual)
Domain Layer
Review Entity
File: /Users/ssg/project/worker/lib/features/reviews/domain/entities/review.dart
class Review {
final String id; // Review ID (format: ITEM-{item_id}-{user_email})
final String itemId; // Product item code
final double rating; // API rating (0-1 scale)
final String comment; // Review text
final String? reviewerName; // Reviewer name
final String? reviewerEmail; // Reviewer email
final DateTime? reviewDate; // Review date
// Convert API rating (0-1) to stars (0-5)
int get starsRating => (rating * 5).round();
// Get exact decimal rating (0-5)
double get starsRatingDecimal => rating * 5;
}
Repository Interface
File: /Users/ssg/project/worker/lib/features/reviews/domain/repositories/reviews_repository.dart
abstract class ReviewsRepository {
Future<List<Review>> getProductReviews({
required String itemId,
int limitPageLength = 10,
int limitStart = 0,
});
Future<void> submitReview({
required String itemId,
required double rating,
required String comment,
String? name,
});
Future<void> deleteReview({required String name});
}
Use Cases
- GetProductReviews: Fetches reviews with pagination
- SubmitReview: Creates or updates a review (validates rating 0-1, comment 20-1000 chars)
- DeleteReview: Deletes a review by ID
Data Layer
Review Model
File: /Users/ssg/project/worker/lib/features/reviews/data/models/review_model.dart
Features:
- JSON serialization with
fromJson()andtoJson() - Entity conversion with
toEntity()andfromEntity() - Email-to-name extraction fallback (e.g., "john.doe@example.com" → "John Doe")
- DateTime parsing for both ISO 8601 and Frappe formats
- Handles multiple response field names (
owner_full_name,full_name)
Assumed API Response Format:
{
"name": "ITEM-GIB20 G04-user@example.com",
"item_id": "GIB20 G04",
"rating": 0.8,
"comment": "Great product!",
"owner": "user@example.com",
"owner_full_name": "John Doe",
"creation": "2024-11-17 10:30:00",
"modified": "2024-11-17 10:30:00"
}
Remote Data Source
File: /Users/ssg/project/worker/lib/features/reviews/data/datasources/reviews_remote_datasource.dart
Features:
- DioClient integration with interceptors
- Comprehensive error handling:
- Network errors (timeout, no internet, connection)
- HTTP status codes (400, 401, 403, 404, 409, 429, 5xx)
- Frappe-specific error extraction from response
- Multiple response format handling:
{ "message": [...] }{ "message": { "data": [...] } }{ "data": [...] }- Direct array
[...]
Repository Implementation
File: /Users/ssg/project/worker/lib/features/reviews/data/repositories/reviews_repository_impl.dart
Features:
- Converts models to entities
- Sorts reviews by date (newest first)
- Delegates to remote data source
Presentation Layer
Riverpod Providers
File: /Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.dart
Data Layer Providers:
reviewsRemoteDataSourceProvider: Remote data source instancereviewsRepositoryProvider: Repository instance
Use Case Providers:
getProductReviewsProvider: Get reviews use casesubmitReviewProvider: Submit review use casedeleteReviewProvider: Delete review use case
State Providers:
// Fetch reviews for a product
final reviewsAsync = ref.watch(productReviewsProvider(itemId));
// Calculate average rating
final avgRating = ref.watch(productAverageRatingProvider(itemId));
// Get review count
final count = ref.watch(productReviewCountProvider(itemId));
// Check if user can submit review
final canSubmit = ref.watch(canSubmitReviewProvider(itemId));
Helper Functions:
// Convert UI stars to API rating
double apiRating = starsToApiRating(5); // 1.0
// Convert API rating to UI stars
int stars = apiRatingToStars(0.8); // 4
UI Updates
1. ProductTabsSection Widget
File: /Users/ssg/project/worker/lib/features/products/presentation/widgets/product_detail/product_tabs_section.dart
Changes:
- Changed
_ReviewsTabfromStatelessWidgettoConsumerWidget - Replaced mock reviews with
productReviewsProvider - Added real-time average rating calculation
- Implemented loading, error, and empty states
- Updated
_ReviewItemto useReviewentity instead ofMap - Added date formatting function (
_formatDate)
States:
- Loading: Shows CustomLoadingIndicator
- Error: Shows error icon and message
- Empty: Shows "Chưa có đánh giá nào" with call-to-action
- Data: Shows rating overview and review list
Rating Overview:
- Dynamic average rating display (0-5 scale)
- Star rendering with full/half/empty stars
- Review count from actual data
Review Cards:
- Reviewer name (with fallback to "Người dùng")
- Relative date formatting (e.g., "2 ngày trước", "1 tuần trước")
- Star rating (converted from 0-1 to 5 stars)
- Comment text
2. WriteReviewPage
File: /Users/ssg/project/worker/lib/features/products/presentation/pages/write_review_page.dart
Changes:
- Added
submitReviewProviderusage - Implemented real API submission with error handling
- Added rating conversion (stars → API rating)
- Invalidates
productReviewsProviderafter successful submission - Shows success/error SnackBars with appropriate icons
Submit Flow:
- Validate form (rating 1-5, comment 20-1000 chars)
- Convert rating:
apiRating = _selectedRating / 5.0 - Call API via
submitReviewuse case - On success:
- Show success SnackBar
- Invalidate reviews cache (triggers refresh)
- Navigate back to product detail
- On error:
- Show error SnackBar
- Keep user on page to retry
API Constants Updated
File: /Users/ssg/project/worker/lib/core/constants/api_constants.dart
Added three new constants:
static const String frappeGetReviews =
'/building_material.building_material.api.item_feedback.get_list';
static const String frappeUpdateReview =
'/building_material.building_material.api.item_feedback.update';
static const String frappeDeleteReview =
'/building_material.building_material.api.item_feedback.delete';
Error Handling
Network Errors
- NoInternetException: "Không có kết nối internet"
- TimeoutException: "Kết nối quá lâu. Vui lòng thử lại."
- ServerException: "Lỗi máy chủ. Vui lòng thử lại sau."
HTTP Status Codes
- 400: BadRequestException - "Dữ liệu không hợp lệ"
- 401: UnauthorizedException - "Phiên đăng nhập hết hạn"
- 403: ForbiddenException - "Không có quyền truy cập"
- 404: NotFoundException - "Không tìm thấy đánh giá"
- 409: ConflictException - "Đánh giá đã tồn tại"
- 429: RateLimitException - "Quá nhiều yêu cầu"
- 500+: ServerException - Custom message from API
Validation Errors
- Rating must be 0-1 (API scale)
- Comment must be 20-1000 characters
- Comment cannot be empty
Authentication Requirements
All review API endpoints require:
- Cookie:
sid={session_id}(from auth flow) - Header:
X-Frappe-Csrf-Token: {csrf_token}(from auth flow)
These are handled automatically by the AuthInterceptor in the DioClient configuration.
Review ID Format
The review ID (name field) follows this pattern:
ITEM-{item_id}-{user_email}
Examples:
ITEM-GIB20 G04-john.doe@example.comITEM-Gạch ốp Signature SIG.P-8806-user@company.com
This ID is used for:
- Identifying reviews in the system
- Updating existing reviews (pass as
nameparameter) - Deleting reviews
Pagination Support
The getProductReviews endpoint supports pagination:
// Fetch first 10 reviews
final reviews = await repository.getProductReviews(
itemId: 'GIB20 G04',
limitPageLength: 10,
limitStart: 0,
);
// Fetch next 10 reviews
final moreReviews = await repository.getProductReviews(
itemId: 'GIB20 G04',
limitPageLength: 10,
limitStart: 10,
);
Current Implementation: Fetches 50 reviews at once (can be extended with infinite scroll)
Testing Checklist
API Integration
- Reviews load correctly in ProductTabsSection
- Rating scale conversion works (0-1 ↔ 1-5 stars)
- Submit review works and refreshes list
- Average rating calculated correctly
- Empty state shown when no reviews
- Error handling for API failures
- Loading states shown during API calls
UI/UX
- Review cards display correct information
- Date formatting works correctly (relative dates)
- Star ratings display correctly
- Write review button navigates correctly
- Submit button disabled during submission
- Success/error messages shown appropriately
Edge Cases
- Handle missing reviewer name (fallback to email extraction)
- Handle missing review date
- Handle empty review list
- Handle API errors gracefully
- Handle network connectivity issues
Known Issues and Limitations
1. Build Runner
Issue: Cannot run dart run build_runner build due to Dart SDK version mismatch
- Required: Dart 3.10.0
- Available: Dart 3.9.2
Workaround: Manually created reviews_provider.g.dart file
Solution: Upgrade Dart SDK to 3.10.0 and regenerate
2. API Response Format
Issue: Actual API response structure not fully documented
Assumption: Based on common Frappe patterns:
{
"message": [
{
"name": "...",
"item_id": "...",
"rating": 0.5,
"comment": "...",
"owner": "...",
"creation": "..."
}
]
}
Recommendation: Test with actual API and adjust ReviewModel.fromJson() if needed
3. One Review Per User
Current: Users can submit multiple reviews for the same product
Future Enhancement:
- Check if user already reviewed product
- Update
canSubmitReviewProviderto enforce one-review-per-user - Show "Edit Review" instead of "Write Review" for existing reviews
4. Review Deletion
Current: Delete functionality implemented but not exposed in UI
Future Enhancement:
- Add "Delete" button for user's own reviews
- Require confirmation dialog
- Refresh list after deletion
Next Steps
Immediate
- Test with Real API: Verify actual response format and adjust model if needed
- Upgrade Dart SDK: To 3.10.0 for proper code generation
- Run Build Runner: Regenerate provider code automatically
Short-term
- Add Review Editing: Allow users to edit their own reviews
- Add Review Deletion UI: Show delete button for user's reviews
- Implement Pagination: Add "Load More" button for reviews
- Add Helpful Button: Allow users to mark reviews as helpful
- Add Review Images: Support photo uploads in reviews
Long-term
- Review Moderation: Admin panel for reviewing flagged reviews
- Verified Purchase Badge: Show badge for reviews from verified purchases
- Review Sorting: Sort by date, rating, helpful votes
- Review Filtering: Filter by star rating
- Review Analytics: Show rating distribution graph
File Paths Reference
All file paths are absolute for easy navigation:
Domain Layer:
/Users/ssg/project/worker/lib/features/reviews/domain/entities/review.dart/Users/ssg/project/worker/lib/features/reviews/domain/repositories/reviews_repository.dart/Users/ssg/project/worker/lib/features/reviews/domain/usecases/get_product_reviews.dart/Users/ssg/project/worker/lib/features/reviews/domain/usecases/submit_review.dart/Users/ssg/project/worker/lib/features/reviews/domain/usecases/delete_review.dart
Data Layer:
/Users/ssg/project/worker/lib/features/reviews/data/models/review_model.dart/Users/ssg/project/worker/lib/features/reviews/data/datasources/reviews_remote_datasource.dart/Users/ssg/project/worker/lib/features/reviews/data/repositories/reviews_repository_impl.dart
Presentation Layer:
/Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.dart/Users/ssg/project/worker/lib/features/reviews/presentation/providers/reviews_provider.g.dart
Updated Files:
/Users/ssg/project/worker/lib/features/products/presentation/widgets/product_detail/product_tabs_section.dart/Users/ssg/project/worker/lib/features/products/presentation/pages/write_review_page.dart/Users/ssg/project/worker/lib/core/constants/api_constants.dart
Code Examples
Fetching Reviews in a Widget
@override
Widget build(BuildContext context, WidgetRef ref) {
final reviewsAsync = ref.watch(productReviewsProvider('PRODUCT_ID'));
return reviewsAsync.when(
data: (reviews) {
if (reviews.isEmpty) {
return Text('No reviews yet');
}
return ListView.builder(
itemCount: reviews.length,
itemBuilder: (context, index) {
final review = reviews[index];
return ListTile(
title: Text(review.reviewerName ?? 'Anonymous'),
subtitle: Text(review.comment),
leading: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
5,
(i) => Icon(
i < review.starsRating
? Icons.star
: Icons.star_border,
),
),
),
);
},
);
},
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
Submitting a Review
Future<void> submitReview(WidgetRef ref, String productId, int stars, String comment) async {
try {
final submitUseCase = ref.read(submitReviewProvider);
// Convert stars (1-5) to API rating (0-1)
final apiRating = stars / 5.0;
await submitUseCase(
itemId: productId,
rating: apiRating,
comment: comment,
);
// Refresh reviews list
ref.invalidate(productReviewsProvider(productId));
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Review submitted successfully!')),
);
} catch (e) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
Getting Average Rating
@override
Widget build(BuildContext context, WidgetRef ref) {
final avgRatingAsync = ref.watch(productAverageRatingProvider('PRODUCT_ID'));
return avgRatingAsync.when(
data: (avgRating) => Text(
'Average: ${avgRating.toStringAsFixed(1)} stars',
),
loading: () => Text('Loading...'),
error: (_, __) => Text('No ratings yet'),
);
}
Conclusion
The review API integration is complete and ready for testing with the real backend. The implementation follows clean architecture principles, uses Riverpod for state management, and includes comprehensive error handling.
Key Achievements:
- ✅ Complete clean architecture implementation (domain, data, presentation layers)
- ✅ Type-safe API client with comprehensive error handling
- ✅ Rating scale conversion (0-1 ↔ 1-5 stars)
- ✅ Real-time UI updates with Riverpod
- ✅ Loading, error, and empty states
- ✅ Form validation and user feedback
- ✅ Date formatting and name extraction
- ✅ Pagination support
Next Action: Test with real API endpoints and adjust response parsing if needed.