Flutter Architecture Patterns (MVVM & Clean Architecture)
7 questions foundWhat is the MVVM, or Model View ViewModel, architectural pattern, and how does it help clearly separate a Flutter app's user interface code from its underlying business logic?
Beginner MVVM separates an application into three distinct, clearly defined parts, the Model representing the actual underlying data and core business rules, the View representing the visual widgets actually responsible for displaying that data to the user, and the ViewModel acting as an intermediary that prepares and exposes data specifically formatted for the View while also handling any user interactions, and this clean separation means the View itself can generally remain fairly simple and focused purely on layout and presentation, while the ViewModel handles the more meaningfully complex actual business logic, making both considerably easier to test and maintain fully independently of each other.
class ProductViewModel extends ChangeNotifier {
List<Product> products = [];
Future<void> loadProducts() async {
products = await repository.fetchProducts();
notifyListeners();
}
}
Real-world example A product listing screen keeps its actual widget code focused purely on layout and displaying data, while a separate ProductViewModel handles fetching, sorting, and properly filtering the underlying product data, making the actual business logic considerably easier to test entirely independently of the user interface itself.
Common follow-ups: How does the ViewModel actually communicate a state change back to the View that needs to rebuild?;What is the difference between MVVM and the closely related MVC pattern?
State Management with Provider;Unit Testing in Flutter
What is Clean Architecture, and what are its three commonly referenced core layers, presentation, domain, and data?
Beginner Clean Architecture is a broader architectural philosophy organizing an application into distinct concentric layers, with the presentation layer handling the actual user interface, the domain layer at the very core containing pure genuine business logic and rules that remain completely independent of any specific external framework or technology, and the data layer handling the specific technical details of actually fetching and persisting data from a database or a network API, and a core underlying principle is that dependencies should always point inward toward that central domain layer, meaning the genuine core business logic itself never actually needs to know anything specific about, for example, exactly which particular database or networking library your app happens to actually be using underneath.
presentation/ -> domain/ <- data/
// Dependencies point inward, toward the domain layer
Real-world example A team building a genuinely long lived enterprise app adopts Clean Architecture specifically so their core actual business rules remain completely unaffected even if they later decide to switch their underlying database technology or replace their networking library entirely.
Common follow-ups: What does it actually mean in practice for dependencies to point inward toward the domain layer?;Is Clean Architecture generally considered overkill for a smaller, simpler application?
Flutter App Architecture with Modular Feature Folders;Dependency Injection in Flutter (GetIt & Service Locator)
How do use cases, sometimes also called interactors, in Clean Architecture help encapsulate one single specific piece of application business logic, keeping it fully independent of any specific user interface or specific data source implementation?
Intermediate A use case represents one single specific, clearly defined piece of application business logic, such as placing an order or calculating an applicable discount, exposing one clear, focused method that a ViewModel or presenter calls, and this use case itself depends only on abstract repository interfaces rather than any concrete, specific implementation, meaning the exact same core use case logic can genuinely be reused across several different specific user interface screens, and more easily and thoroughly tested in complete isolation without needing to also involve or set up any actual concrete networking or database implementation at all.
class PlaceOrderUseCase {
final OrderRepository repository;
PlaceOrderUseCase(this.repository);
Future<Order> execute(Cart cart) => repository.createOrder(cart);
}
Real-world example An e commerce app defines a single PlaceOrderUseCase that is genuinely reused identically by both the main checkout screen and a completely separate quick reorder feature, avoiding duplicating that exact same core order placement business logic in two entirely separate places.
Common follow-ups: How granular should a single use case genuinely be, in terms of how much specific logic it should actually contain?;How do use cases actually interact with a state management solution like Provider or Riverpod?
Dependency Injection in Flutter (GetIt & Service Locator);Unit Testing in Flutter
How does the repository pattern abstract away the specific technical details of exactly where data actually comes from, such as a remote API versus a local cache, from the rest of an app's business logic?
Intermediate A repository defines an abstract interface describing what specific data operations are actually available, such as fetching a list of products or saving a specific user's completed order, while its own concrete internal implementation handles the genuine technical details of exactly where that data actually comes from, whether that means a live remote API call, a locally cached database query, or potentially some combination of both, and the rest of your app's own business logic only ever needs to depend on that abstract repository interface itself, meaning the underlying data source implementation can later be freely and easily changed, such as adding local caching, without requiring any change whatsoever to the actual business logic that genuinely depends on it.
abstract class ProductRepository {
Future<List<Product>> getProducts();
}
class ApiProductRepository implements ProductRepository {
@override
Future<List<Product>> getProducts() => apiClient.fetchProducts();
}
Real-world example A shopping app later adds a local caching layer to its existing ProductRepository implementation without needing to modify a single line of the actual business logic that already depends on that repository's abstract interface, since that logic only genuinely cares about its defined abstract contract rather than its specific concrete internal implementation.
Common follow-ups: How do you properly test business logic that depends on an abstract repository interface, without needing an actual real implementation?;What is the difference between a repository and simply calling an API client's own methods directly?
Networking with HTTP & Dio;Cloud Firestore & Firebase Storage
How does properly separating business logic from a specific state management library, such as Provider or BLoC, help make that business logic genuinely reusable and considerably easier to test, regardless of which particular library you might actually choose to use?
Intermediate Keeping your core actual business logic, such as use cases and repositories, fully independent of any specific chosen state management library means that logic can genuinely be thoroughly unit tested completely on its own, without needing to also set up or involve any actual specific Flutter widget or state management framework at all, and it also means your specific state management choice becomes considerably more of a genuinely replaceable implementation detail, since your core actual business logic itself does not depend on or even know anything about that particular chosen library, making a future potential migration between different state management approaches, such as from Provider to Riverpod, considerably less disruptive and risky overall.
// Business logic has no dependency on Provider, BLoC, or any specific UI framework
class OrderService {
Future<Order> placeOrder(Cart cart) { /* pure business logic */ }
}
Real-world example A team successfully migrates their entire app's state management from Provider to Riverpod over the course of just a few weeks, made considerably easier specifically because their core actual business logic had always remained fully independent of either specific state management library the whole time.
Common follow-ups: What specific parts of a typical Flutter app genuinely cannot be made fully independent of a chosen state management library?;How much additional genuine effort does properly maintaining this specific separation actually require on an ongoing basis?
State Management with Provider;State Management with Riverpod
What are the practical tradeoffs of adopting a genuinely full, strict Clean Architecture approach for a Flutter app, in terms of both the additional required boilerplate code and the genuine long term maintainability benefits it actually provides?
Advanced Adopting a genuinely full, strict Clean Architecture approach typically requires noticeably more upfront boilerplate code, such as defining separate abstract repository interfaces, individual dedicated use case classes, and distinct data models for each of the separate presentation, domain, and data layers, which can meaningfully slow down initial development for a smaller or simpler application, but this same additional upfront structure generally pays off considerably as an application's genuine complexity and overall codebase size grow substantially over time, since the resulting clear separation of distinct concerns makes the codebase considerably easier to test thoroughly, to onboard new developers into effectively, and to safely and confidently modify without unintentionally introducing new far reaching, unexpected bugs elsewhere.
// Full Clean Architecture: significantly more files, but clearer separation
// domain/entities/order.dart
// domain/repositories/order_repository.dart
// domain/usecases/place_order_usecase.dart
// data/repositories/order_repository_impl.dart
Real-world example A startup building an early stage minimum viable product deliberately chooses a considerably lighter weight architecture initially to genuinely maximize their development speed, explicitly planning to properly adopt a more full, strict Clean Architecture approach only later, once their product has actually found genuine, sustained market traction.
Common follow-ups: How do you properly decide on an appropriate level of architectural strictness for a specific given project's actual needs?;Can a Flutter app reasonably adopt some, but genuinely not all, of Clean Architecture's core principles?
Flutter App Architecture with Modular Feature Folders;Dependency Injection in Flutter (GetIt & Service Locator)
How do you properly test each individual distinct layer of a Clean Architecture based Flutter app, including the domain, data, and presentation layers, using genuinely appropriate distinct testing strategies for each one?
Advanced Testing a Clean Architecture based app typically involves using several genuinely distinct, appropriate testing strategies for each individual separate layer, with the domain layer's use cases being thoroughly unit tested using simple mock repository implementations, since that core layer contains only pure genuine business logic with absolutely no external dependencies at all, the data layer's specific repository implementations being tested against either a properly mocked network client or a dedicated test database, and the presentation layer being tested using Flutter's own dedicated widget testing tools to verify the user interface correctly and appropriately responds to different specific states the ViewModel might actually produce.
test('PlaceOrderUseCase successfully calls the repository', () async {
final mockRepo = MockOrderRepository();
final useCase = PlaceOrderUseCase(mockRepo);
await useCase.execute(testCart);
verify(mockRepo.createOrder(testCart)).called(1);
});
Real-world example A team achieves genuinely high overall test coverage on their checkout feature by unit testing their domain layer's use cases in complete isolation, separately testing their data layer's repository implementation against a properly mocked API, and using widget tests specifically to verify their checkout screen's own user interface behavior.
Common follow-ups: What percentage of an application's total tests should ideally genuinely be unit tests versus widget tests versus full integration tests?;How does properly testing the data layer specifically differ from testing the core domain layer?
Unit Testing in Flutter;Widget Testing in Flutter