528 lines
25 KiB
Markdown
528 lines
25 KiB
Markdown
# Reviews Feature - Architecture Diagram
|
|
|
|
## Clean Architecture Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ PRESENTATION LAYER │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ UI Components │ │
|
|
│ │ - ProductTabsSection (_ReviewsTab) │ │
|
|
│ │ - WriteReviewPage │ │
|
|
│ │ - _ReviewItem widget │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
│ │ watches providers │
|
|
│ ┌───────────────▼───────────────────────────────────────────┐ │
|
|
│ │ Riverpod Providers (reviews_provider.dart) │ │
|
|
│ │ - productReviewsProvider(itemId) │ │
|
|
│ │ - productAverageRatingProvider(itemId) │ │
|
|
│ │ - productReviewCountProvider(itemId) │ │
|
|
│ │ - submitReviewProvider │ │
|
|
│ │ - deleteReviewProvider │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
└──────────────────┼───────────────────────────────────────────────┘
|
|
│ calls use cases
|
|
┌──────────────────▼───────────────────────────────────────────────┐
|
|
│ DOMAIN LAYER │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Use Cases │ │
|
|
│ │ - GetProductReviews │ │
|
|
│ │ - SubmitReview │ │
|
|
│ │ - DeleteReview │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
│ │ depends on │
|
|
│ ┌───────────────▼───────────────────────────────────────────┐ │
|
|
│ │ Repository Interface (ReviewsRepository) │ │
|
|
│ │ - getProductReviews() │ │
|
|
│ │ - submitReview() │ │
|
|
│ │ - deleteReview() │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ┌───────────────▼───────────────────────────────────────────┐ │
|
|
│ │ Entities │ │
|
|
│ │ - Review │ │
|
|
│ │ - id, itemId, rating, comment │ │
|
|
│ │ - reviewerName, reviewerEmail, reviewDate │ │
|
|
│ │ - starsRating (computed: rating * 5) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
└──────────────────┬───────────────────────────────────────────────┘
|
|
│ implemented by
|
|
┌──────────────────▼───────────────────────────────────────────────┐
|
|
│ DATA LAYER │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Repository Implementation │ │
|
|
│ │ ReviewsRepositoryImpl │ │
|
|
│ │ - delegates to remote data source │ │
|
|
│ │ - converts models to entities │ │
|
|
│ │ - sorts reviews by date │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
│ │ uses │
|
|
│ ┌───────────────▼───────────────────────────────────────────┐ │
|
|
│ │ Remote Data Source (ReviewsRemoteDataSourceImpl) │ │
|
|
│ │ - makes HTTP requests via DioClient │ │
|
|
│ │ - handles response parsing │ │
|
|
│ │ - error handling & transformation │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
│ │ returns │
|
|
│ ┌───────────────▼───────────────────────────────────────────┐ │
|
|
│ │ Models (ReviewModel) │ │
|
|
│ │ - fromJson() / toJson() │ │
|
|
│ │ - toEntity() / fromEntity() │ │
|
|
│ │ - handles API response format │ │
|
|
│ └───────────────┬───────────────────────────────────────────┘ │
|
|
└──────────────────┼───────────────────────────────────────────────┘
|
|
│ communicates with
|
|
┌──────────────────▼───────────────────────────────────────────────┐
|
|
│ EXTERNAL SERVICES │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Frappe/ERPNext API │ │
|
|
│ │ - POST /api/method/...item_feedback.get_list │ │
|
|
│ │ - POST /api/method/...item_feedback.update │ │
|
|
│ │ - POST /api/method/...item_feedback.delete │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow: Fetching Reviews
|
|
|
|
```
|
|
User opens product detail page
|
|
│
|
|
▼
|
|
ProductTabsSection renders
|
|
│
|
|
▼
|
|
_ReviewsTab watches productReviewsProvider(itemId)
|
|
│
|
|
▼
|
|
Provider executes GetProductReviews use case
|
|
│
|
|
▼
|
|
Use case calls repository.getProductReviews()
|
|
│
|
|
▼
|
|
Repository calls remoteDataSource.getProductReviews()
|
|
│
|
|
▼
|
|
Data source makes HTTP POST to API
|
|
│
|
|
▼
|
|
API returns JSON response
|
|
│
|
|
▼
|
|
Data source parses JSON to List<ReviewModel>
|
|
│
|
|
▼
|
|
Repository converts models to List<Review> entities
|
|
│
|
|
▼
|
|
Repository sorts reviews by date (newest first)
|
|
│
|
|
▼
|
|
Provider returns AsyncValue<List<Review>>
|
|
│
|
|
▼
|
|
_ReviewsTab renders reviews with .when()
|
|
│
|
|
▼
|
|
User sees review list
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow: Submitting Review
|
|
|
|
```
|
|
User clicks "Write Review" button
|
|
│
|
|
▼
|
|
Navigate to WriteReviewPage
|
|
│
|
|
▼
|
|
User selects stars (1-5) and enters comment
|
|
│
|
|
▼
|
|
User clicks "Submit" button
|
|
│
|
|
▼
|
|
Page validates form:
|
|
- Rating: 1-5 stars ✓
|
|
- Comment: 20-1000 chars ✓
|
|
│
|
|
▼
|
|
Convert stars to API rating: apiRating = stars / 5.0
|
|
│
|
|
▼
|
|
Call submitReviewProvider.call()
|
|
│
|
|
▼
|
|
Use case validates:
|
|
- Rating: 0-1 ✓
|
|
- Comment: not empty, 20-1000 chars ✓
|
|
│
|
|
▼
|
|
Use case calls repository.submitReview()
|
|
│
|
|
▼
|
|
Repository calls remoteDataSource.submitReview()
|
|
│
|
|
▼
|
|
Data source makes HTTP POST to API
|
|
│
|
|
▼
|
|
API processes request and returns success
|
|
│
|
|
▼
|
|
Data source returns (void)
|
|
│
|
|
▼
|
|
Use case returns (void)
|
|
│
|
|
▼
|
|
Page invalidates productReviewsProvider(itemId)
|
|
│
|
|
▼
|
|
Page shows success SnackBar
|
|
│
|
|
▼
|
|
Page navigates back to product detail
|
|
│
|
|
▼
|
|
ProductTabsSection refreshes (due to invalidate)
|
|
│
|
|
▼
|
|
User sees updated review list with new review
|
|
```
|
|
|
|
---
|
|
|
|
## Rating Scale Conversion Flow
|
|
|
|
```
|
|
UI Layer (Stars: 1-5)
|
|
│
|
|
│ User selects 4 stars
|
|
│
|
|
▼
|
|
Convert to API: 4 / 5.0 = 0.8
|
|
│
|
|
▼
|
|
Domain Layer (Rating: 0-1)
|
|
│
|
|
│ Use case validates: 0 ≤ 0.8 ≤ 1 ✓
|
|
│
|
|
▼
|
|
Data Layer sends: { "rating": 0.8 }
|
|
│
|
|
▼
|
|
API stores: rating = 0.8
|
|
│
|
|
▼
|
|
API returns: { "rating": 0.8 }
|
|
│
|
|
▼
|
|
Data Layer parses: ReviewModel(rating: 0.8)
|
|
│
|
|
▼
|
|
Domain Layer converts: Review(rating: 0.8)
|
|
│
|
|
│ Entity computes: starsRating = (0.8 * 5).round() = 4
|
|
│
|
|
▼
|
|
UI Layer displays: ⭐⭐⭐⭐☆
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling Flow
|
|
|
|
```
|
|
User action (fetch/submit/delete)
|
|
│
|
|
▼
|
|
Try block starts
|
|
│
|
|
▼
|
|
API call may throw exceptions:
|
|
│
|
|
├─► DioException (timeout, connection, etc.)
|
|
│ │
|
|
│ ▼
|
|
│ Caught by _handleDioException()
|
|
│ │
|
|
│ ▼
|
|
│ Converted to app exception:
|
|
│ - TimeoutException
|
|
│ - NoInternetException
|
|
│ - UnauthorizedException
|
|
│ - ServerException
|
|
│ - etc.
|
|
│
|
|
├─► ParseException (JSON parsing error)
|
|
│ │
|
|
│ ▼
|
|
│ Rethrown as-is
|
|
│
|
|
└─► Unknown error
|
|
│
|
|
▼
|
|
UnknownException(originalError, stackTrace)
|
|
│
|
|
▼
|
|
Exception propagates to provider
|
|
│
|
|
▼
|
|
Provider returns AsyncValue.error(exception)
|
|
│
|
|
▼
|
|
UI handles with .when(error: ...)
|
|
│
|
|
▼
|
|
User sees error message
|
|
```
|
|
|
|
---
|
|
|
|
## Provider Dependency Graph
|
|
|
|
```
|
|
dioClientProvider
|
|
│
|
|
▼
|
|
reviewsRemoteDataSourceProvider
|
|
│
|
|
▼
|
|
reviewsRepositoryProvider
|
|
│
|
|
┌────────────┼────────────┐
|
|
▼ ▼ ▼
|
|
getProductReviews submitReview deleteReview
|
|
Provider Provider Provider
|
|
│ │ │
|
|
▼ │ │
|
|
productReviewsProvider│ │
|
|
(family) │ │
|
|
│ │ │
|
|
┌──────┴──────┐ │ │
|
|
▼ ▼ ▼ ▼
|
|
productAverage productReview (used directly
|
|
RatingProvider CountProvider in UI components)
|
|
(family) (family)
|
|
```
|
|
|
|
---
|
|
|
|
## Component Interaction Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ ProductDetailPage │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ ProductTabsSection │ │
|
|
│ │ │ │
|
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
|
|
│ │ │ Specifications│ │ Reviews │ │ (other tab) │ │ │
|
|
│ │ │ Tab │ │ Tab │ │ │ │ │
|
|
│ │ └──────────────┘ └──────┬───────┘ └─────────────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ┌─────────────────────────▼───────────────────────┐ │ │
|
|
│ │ │ _ReviewsTab │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
|
|
│ │ │ │ WriteReviewButton │ │ │ │
|
|
│ │ │ │ (navigates to WriteReviewPage) │ │ │ │
|
|
│ │ │ └───────────────────────────────────────────┘ │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
|
|
│ │ │ │ Rating Overview │ │ │ │
|
|
│ │ │ │ - Average rating (4.8) │ │ │ │
|
|
│ │ │ │ - Star display (⭐⭐⭐⭐⭐) │ │ │ │
|
|
│ │ │ │ - Review count (125 đánh giá) │ │ │ │
|
|
│ │ │ └───────────────────────────────────────────┘ │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
|
|
│ │ │ │ _ReviewItem (repeated) │ │ │ │
|
|
│ │ │ │ - Avatar │ │ │ │
|
|
│ │ │ │ - Reviewer name │ │ │ │
|
|
│ │ │ │ - Date (2 tuần trước) │ │ │ │
|
|
│ │ │ │ - Star rating (⭐⭐⭐⭐☆) │ │ │ │
|
|
│ │ │ │ - Comment text │ │ │ │
|
|
│ │ │ └───────────────────────────────────────────┘ │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ └─────────────────────────────────────────────────┘ │ │
|
|
│ └────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
│ clicks "Write Review"
|
|
▼
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ WriteReviewPage │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ Product Info Card (read-only) │ │
|
|
│ │ - Product image │ │
|
|
│ │ - Product name │ │
|
|
│ │ - Product code │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ StarRatingSelector │ │
|
|
│ │ ☆☆☆☆☆ → ⭐⭐⭐⭐☆ (4 stars selected) │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ Comment TextField │ │
|
|
│ │ [ ] │ │
|
|
│ │ [ Multi-line text input ] │ │
|
|
│ │ [ ] │ │
|
|
│ │ 50 / 1000 ký tự │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ ReviewGuidelinesCard │ │
|
|
│ │ - Be honest and fair │ │
|
|
│ │ - Focus on the product │ │
|
|
│ │ - etc. │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ [Submit Button] │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
│ clicks "Submit"
|
|
▼
|
|
|
|
Validates & submits review
|
|
│
|
|
▼
|
|
Shows success SnackBar
|
|
│
|
|
▼
|
|
Navigates back to ProductDetailPage
|
|
│
|
|
▼
|
|
Reviews refresh automatically
|
|
```
|
|
|
|
---
|
|
|
|
## State Management Lifecycle
|
|
|
|
```
|
|
1. Initial State (Loading)
|
|
├─► productReviewsProvider returns AsyncValue.loading()
|
|
└─► UI shows CustomLoadingIndicator
|
|
|
|
2. Loading State → Data State
|
|
├─► API call succeeds
|
|
├─► Provider returns AsyncValue.data(List<Review>)
|
|
└─► UI shows review list
|
|
|
|
3. Data State → Refresh State (after submit)
|
|
├─► User submits new review
|
|
├─► ref.invalidate(productReviewsProvider)
|
|
├─► Provider state reset to loading
|
|
├─► API call re-executes
|
|
└─► UI updates with new data
|
|
|
|
4. Error State
|
|
├─► API call fails
|
|
├─► Provider returns AsyncValue.error(exception)
|
|
└─► UI shows error message
|
|
|
|
5. Empty State (special case of Data State)
|
|
├─► API returns empty list
|
|
├─► Provider returns AsyncValue.data([])
|
|
└─► UI shows "No reviews yet" message
|
|
```
|
|
|
|
---
|
|
|
|
## Caching Strategy
|
|
|
|
```
|
|
Provider State Cache (Riverpod)
|
|
│
|
|
├─► Auto-disposed when widget unmounted
|
|
│ (productReviewsProvider uses AutoDispose)
|
|
│
|
|
├─► Cache invalidated on:
|
|
│ - User submits review
|
|
│ - User deletes review
|
|
│ - Manual ref.invalidate() call
|
|
│
|
|
└─► Cache refresh:
|
|
- Pull-to-refresh gesture (future enhancement)
|
|
- App resume from background (future enhancement)
|
|
- Time-based expiry (future enhancement)
|
|
|
|
HTTP Cache (Dio CacheInterceptor)
|
|
│
|
|
├─► Reviews NOT cached (POST requests)
|
|
│ (only GET requests cached by default)
|
|
│
|
|
└─► Future: Implement custom cache policy
|
|
- Cache reviews for 5 minutes
|
|
- Invalidate on write operations
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
```
|
|
Unit Tests
|
|
├─► Domain Layer
|
|
│ ├─► Use cases
|
|
│ │ ├─► GetProductReviews
|
|
│ │ ├─► SubmitReview (validates rating & comment)
|
|
│ │ └─► DeleteReview
|
|
│ └─► Entities
|
|
│ └─► Review (starsRating computation)
|
|
│
|
|
├─► Data Layer
|
|
│ ├─► Models (fromJson, toJson, toEntity)
|
|
│ ├─► Remote Data Source (API calls, error handling)
|
|
│ └─► Repository (model-to-entity conversion, sorting)
|
|
│
|
|
└─► Presentation Layer
|
|
└─► Providers (state transformations)
|
|
|
|
Widget Tests
|
|
├─► _ReviewsTab
|
|
│ ├─► Loading state
|
|
│ ├─► Empty state
|
|
│ ├─► Data state
|
|
│ └─► Error state
|
|
│
|
|
├─► _ReviewItem
|
|
│ ├─► Displays correct data
|
|
│ ├─► Date formatting
|
|
│ └─► Star rendering
|
|
│
|
|
└─► WriteReviewPage
|
|
├─► Form validation
|
|
├─► Submit button states
|
|
└─► Error messages
|
|
|
|
Integration Tests
|
|
└─► End-to-end flow
|
|
├─► Fetch reviews
|
|
├─► Submit review
|
|
├─► Verify refresh
|
|
└─► Error scenarios
|
|
```
|
|
|
|
This architecture follows:
|
|
- ✅ Clean Architecture principles
|
|
- ✅ SOLID principles
|
|
- ✅ Dependency Inversion (interfaces in domain layer)
|
|
- ✅ Single Responsibility (each class has one job)
|
|
- ✅ Separation of Concerns (UI, business logic, data separate)
|
|
- ✅ Testability (all layers mockable)
|