Guide for implementing Clean Architecture with Provider in Flutter. Use this when adding new features, screens, or state management to the mobile app.
The Flutter app follows Clean Architecture with Provider state management:
lib/
├── presentation/ # UI Layer (screens, widgets, providers)
├── data/ # Data Layer (repositories, models, data sources)
├── services/ # Platform Services (API, storage, etc.)
└── core/ # Core utilities (constants, theme, etc.)
Data Flow: UI → Provider → Repository → API Service → Backend
lib/data/models/review_model.dart)class ReviewModel {
final int id;
final int userId;
final int productId;
final int rating;
final String comment;
final String? userName;
final DateTime createdAt;
ReviewModel({
required this.id,
required this.userId,
required this.productId,
required this.rating,
required this.comment,
this.userName,
required this.createdAt,
});
factory ReviewModel.fromJson(Map<String, dynamic> json) {
return ReviewModel(
id: json['id'],
userId: json['user_id'],
productId: json['product_id'],
rating: json['rating'],
comment: json['comment'],
userName: json['user_name'],
createdAt: DateTime.parse(json['created_at']),
);
}
Map<String, dynamic> toJson() {
return {
'product_id': productId,
'rating': rating,
'comment': comment,
};
}
}
lib/data/repositories/review_repository.dart)import '../models/review_model.dart';
abstract class ReviewRepository {
Future<List<ReviewModel>> getProductReviews(int productId);
Future<ReviewModel> createReview(ReviewModel review);
Future<void> deleteReview(int reviewId);
}
lib/data/repositories/review_repository_impl.dart)import '../../services/api_service.dart';
import '../models/review_model.dart';
import 'review_repository.dart';
class ReviewRepositoryImpl implements ReviewRepository {
final ApiService _apiService;
ReviewRepositoryImpl(this._apiService);
@override
Future<List<ReviewModel>> getProductReviews(int productId) async {
try {
final response = await _apiService.get('/reviews/product/$productId');
if (response.statusCode == 200) {
final List<dynamic> data = response.data['data'];
return data.map((json) => ReviewModel.fromJson(json)).toList();
}
throw Exception('Failed to load reviews');
} catch (e) {
throw Exception('Error fetching reviews: $e');
}
}
@override
Future<ReviewModel> createReview(ReviewModel review) async {
try {
final response = await _apiService.post(
'/reviews',
data: review.toJson(),
);
if (response.statusCode == 201) {
return ReviewModel.fromJson(response.data['data']);
}
throw Exception('Failed to create review');
} catch (e) {
throw Exception('Error creating review: $e');
}
}
@override
Future<void> deleteReview(int reviewId) async {
try {
final response = await _apiService.delete('/reviews/$reviewId');
if (response.statusCode != 200) {
throw Exception('Failed to delete review');
}
} catch (e) {
throw Exception('Error deleting review: $e');
}
}
}
lib/presentation/providers/review_provider.dart)import 'package:flutter/material.dart';
import '../../data/models/review_model.dart';
import '../../data/repositories/review_repository.dart';
class ReviewProvider with ChangeNotifier {
final ReviewRepository _reviewRepository;
ReviewProvider(this._reviewRepository);
List<ReviewModel> _reviews = [];
bool _isLoading = false;
String? _errorMessage;
List<ReviewModel> get reviews => _reviews;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
Future<void> loadProductReviews(int productId) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
_reviews = await _reviewRepository.getProductReviews(productId);
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<bool> createReview(ReviewModel review) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final newReview = await _reviewRepository.createReview(review);
_reviews.insert(0, newReview);
_isLoading = false;
notifyListeners();
return true;
} catch (e) {
_errorMessage = e.toString();
_isLoading = false;
notifyListeners();
return false;
}
}
Future<bool> deleteReview(int reviewId) async {
try {
await _reviewRepository.deleteReview(reviewId);
_reviews.removeWhere((review) => review.id == reviewId);
notifyListeners();
return true;
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
return false;
}
}
void clearError() {
_errorMessage = null;
notifyListeners();
}
}
lib/presentation/screens/reviews/product_reviews_screen.dart)import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/review_provider.dart';
class ProductReviewsScreen extends StatefulWidget {
final int productId;
final String productName;
const ProductReviewsScreen({
Key? key,
required this.productId,
required this.productName,
}) : super(key: key);
@override
State<ProductReviewsScreen> createState() => _ProductReviewsScreenState();
}
class _ProductReviewsScreenState extends State<ProductReviewsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ReviewProvider>().loadProductReviews(widget.productId);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.productName} Reviews'),
),
body: Consumer<ReviewProvider>(
builder: (context, provider, child) {
if (provider.isLoading && provider.reviews.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Error: ${provider.errorMessage}',
style: const TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: () => provider.loadProductReviews(widget.productId),
child: const Text('Retry'),
),
],
),
);
}
if (provider.reviews.isEmpty) {
return const Center(child: Text('No reviews yet'));
}
return RefreshIndicator(
onRefresh: () => provider.loadProductReviews(widget.productId),
child: ListView.builder(
itemCount: provider.reviews.length,
itemBuilder: (context, index) {
final review = provider.reviews[index];
return _ReviewCard(review: review);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreateReviewDialog(),
child: const Icon(Icons.add),
),
);
}
void _showCreateReviewDialog() {
// Show dialog to create review
// ...
}
}
class _ReviewCard extends StatelessWidget {
final ReviewModel review;
const _ReviewCard({Key? key, required this.review}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
review.userName ?? 'User ${review.userId}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
_buildRatingStars(review.rating),
],
),
const SizedBox(height: 8),
Text(review.comment),
const SizedBox(height: 8),
Text(
_formatDate(review.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
Widget _buildRatingStars(int rating) {
return Row(
children: List.generate(5, (index) {
return Icon(
index < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 20,
);
}),
);
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
}
lib/main.dart)import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'presentation/providers/auth_provider.dart';
import 'presentation/providers/review_provider.dart';
import 'data/repositories/auth_repository_impl.dart';
import 'data/repositories/review_repository_impl.dart';
import 'services/api_service.dart';
void main() {
final apiService = ApiService();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => AuthProvider(AuthRepositoryImpl(apiService)),
),
ChangeNotifierProvider(
create: (_) => ReviewProvider(ReviewRepositoryImpl(apiService)),
),
// Add more providers here
],
child: const MyApp(),
),
);
}
lib/app/routes.dart)import 'package:go_router/go_router.dart';
import '../presentation/screens/reviews/product_reviews_screen.dart';
final router = GoRouter(
routes: [
// ... existing routes ...
GoRoute(
path: '/products/:id/reviews',
builder: (context, state) {
final productId = int.parse(state.pathParameters['id']!);
final productName = state.extra as String? ?? 'Product';
return ProductReviewsScreen(
productId: productId,
productName: productName,
);
},
),
],
);
fromJson and toJson methodslib/data/models/ChangeNotifiercontext.read<T>() (one-time)context.watch<T>() or Consumer<T>class MyProvider with ChangeNotifier {
bool _isLoading = false;
String? _errorMessage;
Data? _data;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
Data? get data => _data;
Future<void> loadData() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
_data = await repository.fetchData();
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// Use Consumer for specific widget rebuilds
Consumer<MyProvider>(
builder: (context, provider, child) {
return Text(provider.data);
},
)
// Use context.watch in build method for full widget rebuild
Widget build(BuildContext context) {
final data = context.watch<MyProvider>().data;
return Text(data);
}
// Use context.read for one-time reads or in callbacks
onPressed: () {
context.read<MyProvider>().loadData();
}
// Always use FlutterSecureStorage, never SharedPreferences
final storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: token);
final token = await storage.read(key: 'auth_token');
try {
final response = await apiService.get('/endpoint');
return Model.fromJson(response.data);
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw Exception('Unauthorized');
}
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: $e');
}
// Using go_router
context.go('/path');
context.push('/path');
context.pop();
// With parameters
context.go('/products/${productId}');
// With extras
context.push('/reviews', extra: productData);
// Bad: One giant widget
class ProductScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 200 lines of code...
],
),
);
}
}
// Good: Break into smaller widgets
class ProductScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ProductHeader(),
ProductImages(),
ProductDetails(),
ProductReviews(),
],
),
);
}
}
// Performance optimization
const Text('Static text');
const Icon(Icons.star);
const SizedBox(height: 16);
@override
void initState() {
super.initState();
// Wait for first frame before calling providers
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MyProvider>().loadData();
});
}
test('should load data successfully', () async {
final mockRepo = MockRepository();
final provider = MyProvider(mockRepo);
when(mockRepo.fetchData()).thenAnswer((_) async => testData);
await provider.loadData();
expect(provider.data, equals(testData));
expect(provider.isLoading, isFalse);
});
testWidgets('displays loading indicator', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: MyScreen(),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
lib/data/models/lib/data/repositories/lib/presentation/providers/lib/presentation/screens/lib/main.dartlib/app/routes.dart❌ Using context.read() in build method → Use context.watch() or Consumer
❌ Storing sensitive data in SharedPreferences → Use FlutterSecureStorage
❌ Not calling notifyListeners() → State won't update
❌ Making API calls directly in widgets → Use repositories
❌ Forgetting to dispose controllers → Memory leaks
❌ Hardcoding API URLs → Use constants from lib/core/constants/