# Reviews API - Code Examples ## Table of Contents 1. [Basic Usage](#basic-usage) 2. [Advanced Scenarios](#advanced-scenarios) 3. [Error Handling](#error-handling) 4. [Custom Widgets](#custom-widgets) 5. [Testing Examples](#testing-examples) --- ## Basic Usage ### Display Reviews in a List ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class ReviewsListPage extends ConsumerWidget { const ReviewsListPage({super.key, required this.productId}); final String productId; @override Widget build(BuildContext context, WidgetRef ref) { final reviewsAsync = ref.watch(productReviewsProvider(productId)); return Scaffold( appBar: AppBar(title: const Text('Reviews')), body: reviewsAsync.when( data: (reviews) { if (reviews.isEmpty) { return const Center( child: 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), trailing: Row( mainAxisSize: MainAxisSize.min, children: List.generate( 5, (i) => Icon( i < review.starsRating ? Icons.star : Icons.star_border, size: 16, color: Colors.amber, ), ), ), ); }, ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stack) => Center( child: Text('Error: $error'), ), ), ); } } ``` ### Show Average Rating ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class ProductRatingWidget extends ConsumerWidget { const ProductRatingWidget({super.key, required this.productId}); final String productId; @override Widget build(BuildContext context, WidgetRef ref) { final avgRatingAsync = ref.watch(productAverageRatingProvider(productId)); final countAsync = ref.watch(productReviewCountProvider(productId)); return Row( children: [ // Average rating avgRatingAsync.when( data: (avgRating) => Text( avgRating.toStringAsFixed(1), style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), loading: () => const Text('--'), error: (_, __) => const Text('0.0'), ), const SizedBox(width: 8), // Stars avgRatingAsync.when( data: (avgRating) => Row( children: List.generate(5, (index) { if (index < avgRating.floor()) { return const Icon(Icons.star, color: Colors.amber); } else if (index < avgRating) { return const Icon(Icons.star_half, color: Colors.amber); } else { return const Icon(Icons.star_border, color: Colors.amber); } }), ), loading: () => const SizedBox(), error: (_, __) => const SizedBox(), ), const SizedBox(width: 8), // Review count countAsync.when( data: (count) => Text('($count reviews)'), loading: () => const Text(''), error: (_, __) => const Text(''), ), ], ); } } ``` ### Submit a Review ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class SimpleReviewForm extends ConsumerStatefulWidget { const SimpleReviewForm({super.key, required this.productId}); final String productId; @override ConsumerState createState() => _SimpleReviewFormState(); } class _SimpleReviewFormState extends ConsumerState { int _selectedRating = 0; final _commentController = TextEditingController(); bool _isSubmitting = false; @override void dispose() { _commentController.dispose(); super.dispose(); } Future _submitReview() async { if (_selectedRating == 0 || _commentController.text.trim().length < 20) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please select rating and write at least 20 characters'), ), ); return; } setState(() => _isSubmitting = true); try { final submitUseCase = ref.read(submitReviewProvider); // Convert stars (1-5) to API rating (0-1) final apiRating = _selectedRating / 5.0; await submitUseCase( itemId: widget.productId, rating: apiRating, comment: _commentController.text.trim(), ); if (mounted) { // Refresh reviews list ref.invalidate(productReviewsProvider(widget.productId)); // Show success ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Review submitted successfully!'), backgroundColor: Colors.green, ), ); // Clear form setState(() { _selectedRating = 0; _commentController.clear(); }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: $e'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() => _isSubmitting = false); } } } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Star rating selector Row( children: List.generate(5, (index) { final star = index + 1; return IconButton( icon: Icon( star <= _selectedRating ? Icons.star : Icons.star_border, color: Colors.amber, ), onPressed: () => setState(() => _selectedRating = star), ); }), ), const SizedBox(height: 16), // Comment field TextField( controller: _commentController, maxLines: 5, maxLength: 1000, decoration: const InputDecoration( hintText: 'Write your review...', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), // Submit button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSubmitting ? null : _submitReview, child: _isSubmitting ? const CircularProgressIndicator() : const Text('Submit Review'), ), ), ], ), ); } } ``` --- ## Advanced Scenarios ### Paginated Reviews List ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/domain/entities/review.dart'; import 'package:worker/features/reviews/domain/usecases/get_product_reviews.dart'; class PaginatedReviewsList extends ConsumerStatefulWidget { const PaginatedReviewsList({super.key, required this.productId}); final String productId; @override ConsumerState createState() => _PaginatedReviewsListState(); } class _PaginatedReviewsListState extends ConsumerState { final List _reviews = []; int _currentPage = 0; final int _pageSize = 10; bool _isLoading = false; bool _hasMore = true; @override void initState() { super.initState(); _loadMoreReviews(); } Future _loadMoreReviews() async { if (_isLoading || !_hasMore) return; setState(() => _isLoading = true); try { final getReviews = ref.read(getProductReviewsProvider); final newReviews = await getReviews( itemId: widget.productId, limitPageLength: _pageSize, limitStart: _currentPage * _pageSize, ); setState(() { _reviews.addAll(newReviews); _currentPage++; _hasMore = newReviews.length == _pageSize; _isLoading = false; }); } catch (e) { setState(() => _isLoading = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error loading reviews: $e')), ); } } } @override Widget build(BuildContext context) { return ListView.builder( itemCount: _reviews.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == _reviews.length) { // Load more button return Padding( padding: const EdgeInsets.all(16), child: Center( child: _isLoading ? const CircularProgressIndicator() : ElevatedButton( onPressed: _loadMoreReviews, child: const Text('Load More'), ), ), ); } final review = _reviews[index]; return ListTile( title: Text(review.reviewerName ?? 'Anonymous'), subtitle: Text(review.comment), leading: CircleAvatar( child: Text('${review.starsRating}'), ), ); }, ); } } ``` ### Pull-to-Refresh Reviews ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class RefreshableReviewsList extends ConsumerWidget { const RefreshableReviewsList({super.key, required this.productId}); final String productId; @override Widget build(BuildContext context, WidgetRef ref) { final reviewsAsync = ref.watch(productReviewsProvider(productId)); return RefreshIndicator( onRefresh: () async { // Invalidate provider to trigger refresh ref.invalidate(productReviewsProvider(productId)); // Wait for data to load await ref.read(productReviewsProvider(productId).future); }, child: reviewsAsync.when( data: (reviews) { if (reviews.isEmpty) { // Must return a scrollable widget for RefreshIndicator return ListView( children: const [ Center( child: Padding( padding: EdgeInsets.all(40), child: 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), ); }, ); }, loading: () => ListView( children: const [ Center( child: Padding( padding: EdgeInsets.all(40), child: CircularProgressIndicator(), ), ), ], ), error: (error, stack) => ListView( children: [ Center( child: Padding( padding: const EdgeInsets.all(40), child: Text('Error: $error'), ), ), ], ), ), ); } } ``` ### Filter Reviews by Rating ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/domain/entities/review.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class FilteredReviewsList extends ConsumerStatefulWidget { const FilteredReviewsList({super.key, required this.productId}); final String productId; @override ConsumerState createState() => _FilteredReviewsListState(); } class _FilteredReviewsListState extends ConsumerState { int? _filterByStar; // null = all reviews List _filterReviews(List reviews) { if (_filterByStar == null) return reviews; return reviews.where((review) { return review.starsRating == _filterByStar; }).toList(); } @override Widget build(BuildContext context) { final reviewsAsync = ref.watch(productReviewsProvider(widget.productId)); return Column( children: [ // Filter chips SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.all(8), child: Row( children: [ FilterChip( label: const Text('All'), selected: _filterByStar == null, onSelected: (_) => setState(() => _filterByStar = null), ), const SizedBox(width: 8), for (int star = 5; star >= 1; star--) Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Row( children: [ Text('$star'), const Icon(Icons.star, size: 16), ], ), selected: _filterByStar == star, onSelected: (_) => setState(() => _filterByStar = star), ), ), ], ), ), // Reviews list Expanded( child: reviewsAsync.when( data: (reviews) { final filteredReviews = _filterReviews(reviews); if (filteredReviews.isEmpty) { return const Center( child: Text('No reviews match the filter'), ); } return ListView.builder( itemCount: filteredReviews.length, itemBuilder: (context, index) { final review = filteredReviews[index]; return ListTile( title: Text(review.reviewerName ?? 'Anonymous'), subtitle: Text(review.comment), ); }, ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stack) => Center( child: Text('Error: $error'), ), ), ), ], ); } } ``` --- ## Error Handling ### Comprehensive Error Display ```dart import 'package:flutter/material.dart'; import 'package:worker/core/errors/exceptions.dart'; Widget buildErrorWidget(Object error) { String title; String message; IconData icon; Color color; if (error is NoInternetException) { title = 'No Internet Connection'; message = 'Please check your internet connection and try again.'; icon = Icons.wifi_off; color = Colors.orange; } else if (error is TimeoutException) { title = 'Request Timeout'; message = 'The request took too long. Please try again.'; icon = Icons.timer_off; color = Colors.orange; } else if (error is UnauthorizedException) { title = 'Session Expired'; message = 'Please log in again to continue.'; icon = Icons.lock_outline; color = Colors.red; } else if (error is ServerException) { title = 'Server Error'; message = 'Something went wrong on our end. Please try again later.'; icon = Icons.error_outline; color = Colors.red; } else if (error is ValidationException) { title = 'Invalid Data'; message = error.message; icon = Icons.warning_amber; color = Colors.orange; } else { title = 'Unknown Error'; message = error.toString(); icon = Icons.error; color = Colors.red; } return Center( child: Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 64, color: color), const SizedBox(height: 16), Text( title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: color, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( message, style: const TextStyle(fontSize: 14), textAlign: TextAlign.center, ), ], ), ), ); } ``` ### Retry Logic ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:worker/features/reviews/presentation/providers/reviews_provider.dart'; class ReviewsWithRetry extends ConsumerWidget { const ReviewsWithRetry({super.key, required this.productId}); final String productId; @override Widget build(BuildContext context, WidgetRef ref) { final reviewsAsync = ref.watch(productReviewsProvider(productId)); return reviewsAsync.when( data: (reviews) { // Show reviews return ListView.builder( itemCount: reviews.length, itemBuilder: (context, index) { final review = reviews[index]; return ListTile( title: Text(review.reviewerName ?? 'Anonymous'), subtitle: Text(review.comment), ); }, ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, size: 64, color: Colors.red), const SizedBox(height: 16), Text('Error: $error'), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { // Retry by invalidating provider ref.invalidate(productReviewsProvider(productId)); }, icon: const Icon(Icons.refresh), label: const Text('Retry'), ), ], ), ), ); } } ``` --- ## Custom Widgets ### Custom Review Card ```dart import 'package:flutter/material.dart'; import 'package:worker/features/reviews/domain/entities/review.dart'; class ReviewCard extends StatelessWidget { const ReviewCard({super.key, required this.review}); final Review review; @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header: Avatar + Name + Date Row( children: [ CircleAvatar( child: Text( review.reviewerName?.substring(0, 1).toUpperCase() ?? '?', ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( review.reviewerName ?? 'Anonymous', style: const TextStyle( fontWeight: FontWeight.bold, ), ), if (review.reviewDate != null) Text( _formatDate(review.reviewDate!), style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ), ], ), const SizedBox(height: 12), // Star rating Row( children: List.generate(5, (index) { return Icon( index < review.starsRating ? Icons.star : Icons.star_border, size: 20, color: Colors.amber, ); }), ), const SizedBox(height: 12), // Comment Text( review.comment, style: const TextStyle(height: 1.5), ), ], ), ), ); } String _formatDate(DateTime date) { final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays == 0) return 'Today'; if (diff.inDays == 1) return 'Yesterday'; if (diff.inDays < 7) return '${diff.inDays} days ago'; if (diff.inDays < 30) return '${(diff.inDays / 7).floor()} weeks ago'; if (diff.inDays < 365) return '${(diff.inDays / 30).floor()} months ago'; return '${(diff.inDays / 365).floor()} years ago'; } } ``` ### Star Rating Selector Widget ```dart import 'package:flutter/material.dart'; class StarRatingSelector extends StatelessWidget { const StarRatingSelector({ super.key, required this.rating, required this.onRatingChanged, this.size = 40, this.color = Colors.amber, }); final int rating; final ValueChanged onRatingChanged; final double size; final Color color; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: List.generate(5, (index) { final star = index + 1; return GestureDetector( onTap: () => onRatingChanged(star), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Icon( star <= rating ? Icons.star : Icons.star_border, size: size, color: color, ), ), ); }), ); } } ``` --- ## Testing Examples ### Unit Test for Review Entity ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:worker/features/reviews/domain/entities/review.dart'; void main() { group('Review Entity', () { test('starsRating converts API rating (0-1) to stars (1-5) correctly', () { expect(const Review( id: 'test', itemId: 'item1', rating: 0.2, comment: 'Test', ).starsRating, equals(1)); expect(const Review( id: 'test', itemId: 'item1', rating: 0.5, comment: 'Test', ).starsRating, equals(3)); // 2.5 rounds to 3 expect(const Review( id: 'test', itemId: 'item1', rating: 1.0, comment: 'Test', ).starsRating, equals(5)); }); test('starsRatingDecimal returns exact decimal value', () { expect(const Review( id: 'test', itemId: 'item1', rating: 0.8, comment: 'Test', ).starsRatingDecimal, equals(4.0)); }); }); } ``` ### Widget Test for Review Card ```dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:worker/features/reviews/domain/entities/review.dart'; // Import your ReviewCard widget void main() { testWidgets('ReviewCard displays review data correctly', (tester) async { final review = Review( id: 'test-1', itemId: 'item-1', rating: 0.8, // 4 stars comment: 'Great product!', reviewerName: 'John Doe', reviewDate: DateTime.now().subtract(const Duration(days: 2)), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ReviewCard(review: review), ), ), ); // Verify reviewer name is displayed expect(find.text('John Doe'), findsOneWidget); // Verify comment is displayed expect(find.text('Great product!'), findsOneWidget); // Verify star icons (4 filled, 1 empty) expect(find.byIcon(Icons.star), findsNWidgets(4)); expect(find.byIcon(Icons.star_border), findsOneWidget); // Verify date is displayed expect(find.textContaining('days ago'), findsOneWidget); }); } ``` ### Integration Test for Submit Review ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; // Import your widgets and mocks void main() { testWidgets('Submit review flow', (tester) async { // Setup mock repository final mockRepository = MockReviewsRepository(); when(mockRepository.submitReview( itemId: anyNamed('itemId'), rating: anyNamed('rating'), comment: anyNamed('comment'), )).thenAnswer((_) async {}); await tester.pumpWidget( ProviderScope( overrides: [ reviewsRepositoryProvider.overrideWithValue(mockRepository), ], child: MaterialApp( home: WriteReviewPage(productId: 'test-product'), ), ), ); // Tap the 5th star await tester.tap(find.byIcon(Icons.star_border).last); await tester.pump(); // Enter comment await tester.enterText( find.byType(TextField), 'This is a great product! I highly recommend it.', ); await tester.pump(); // Tap submit button await tester.tap(find.widgetWithText(ElevatedButton, 'Submit')); await tester.pumpAndSettle(); // Verify submit was called with correct parameters verify(mockRepository.submitReview( itemId: 'test-product', rating: 1.0, // 5 stars = 1.0 API rating comment: 'This is a great product! I highly recommend it.', )).called(1); // Verify success message is shown expect(find.text('Review submitted successfully!'), findsOneWidget); }); } ``` These examples cover the most common scenarios and can be adapted to your specific needs!