AgentSkillsCN

flutter-clean-architecture-patterns

在 Flutter 中运用 Provider 实现 Clean Architecture 的指南。适用于为移动应用新增功能、屏幕或状态管理时使用。

SKILL.md
--- frontmatter
name: flutter-clean-architecture-patterns
description: Guide for implementing Clean Architecture with Provider in Flutter. Use this when adding new features, screens, or state management to the mobile app.

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 fromJson and toJson methods
  • 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>() or Consumer<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/