Flutter Clean Architecture Implementation
Architecture Layers
The Flutter app follows Clean Architecture with Provider state management:
code
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
Adding a New Feature (Step-by-Step)
Example: Adding Product Reviews
1. Create Model (lib/data/models/review_model.dart)
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,
};
}
}
2. Create Repository Interface (lib/data/repositories/review_repository.dart)
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);
}
3. Implement Repository (lib/data/repositories/review_repository_impl.dart)
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');
}
}
}
4. Create Provider (lib/presentation/providers/review_provider.dart)
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();
}
}
5. Create Screen (lib/presentation/screens/reviews/product_reviews_screen.dart)
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}';
}
}
6. Register Provider (lib/main.dart)
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(),
),
);
}
7. Add Route (lib/app/routes.dart)
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,
);
},
),
],
);
Key Principles
1. Models
- •Represent data structures
- •Include
fromJsonandtoJsonmethods - •Immutable when possible
- •Located in
lib/data/models/
2. Repositories
- •Define interfaces for data operations
- •Implementations handle API calls
- •Return models, not raw JSON
- •Handle errors and exceptions
3. Providers (State Management)
- •Extend
ChangeNotifier - •Hold app state
- •Call repository methods
- •Notify listeners when state changes
- •Handle loading and error states
4. Screens/Widgets
- •Only handle UI
- •Read state using
context.read<T>()(one-time) - •Watch state using
context.watch<T>()orConsumer<T> - •Keep widgets small and reusable
Common Patterns
Loading, Success, Error States
dart
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();
}
}
}
Consumer vs context.watch vs context.read
dart
// 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();
}
Secure Token Storage
dart
// Always use FlutterSecureStorage, never SharedPreferences final storage = FlutterSecureStorage(); await storage.write(key: 'auth_token', value: token); final token = await storage.read(key: 'auth_token');
API Error Handling
dart
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');
}
Navigation
dart
// Using go_router
context.go('/path');
context.push('/path');
context.pop();
// With parameters
context.go('/products/${productId}');
// With extras
context.push('/reviews', extra: productData);
Widget Best Practices
Keep Widgets Small
dart
// 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(),
],
),
);
}
}
Use const Constructors
dart
// Performance optimization
const Text('Static text');
const Icon(Icons.star);
const SizedBox(height: 16);
Proper State Initialization
dart
@override
void initState() {
super.initState();
// Wait for first frame before calling providers
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MyProvider>().loadData();
});
}
Testing
Provider Tests
dart
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);
});
Widget Tests
dart
testWidgets('displays loading indicator', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: MyScreen(),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
Checklist for New Features
- • Create model in
lib/data/models/ - • Define repository interface in
lib/data/repositories/ - • Implement repository with API calls
- • Create provider in
lib/presentation/providers/ - • Build UI in
lib/presentation/screens/ - • Register provider in
lib/main.dart - • Add routes in
lib/app/routes.dart - • Update API constants if needed
- • Handle loading and error states
- • Add proper error messages
- • Test on different screen sizes
- • Write widget and provider tests
Common Mistakes to Avoid
❌ 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/