Irbisa · Cheatsheet

Architecture Patterns

MVC, MVP, MVVM, Clean Architecture, Repository pattern

Middle Developer · 22 questions

  1. 01

    Compare MVC, MVP, MVVM, and MVI patterns and how they map to Flutter.

    Four ways to split UI from logic.

  2. 02

    Explain Clean Architecture in Flutter with layers and responsibilities.

    Clean Architecture splits an app into three layers with one rule: dependencies point inward only.

    // domain: pure Dart, no Flutter or package importsclass Product {  const Product({required this.id, required this.name, required this.price});  final int id;  final String name;  final double price;…
  3. 03

    What does Flutter's official architecture guide recommend, and how does it differ from Clean Architecture?

    The guide on docs.flutter.dev recommends MVVM in two layers: a UI layer of views and view models, and a data layer of repositories and services.

  4. 04

    What does it mean to apply SOLID principles in Flutter?

    SOLID is five rules for keeping classes small and swappable.

  5. 05

    When is Clean Architecture the wrong choice for a Flutter app, and what do you keep instead?

    In a small or CRUD-style app: when you cannot name the change a layer would isolate, that layer is pure cost, and there it is most of the code.

  6. 06

    How do you map between domain entities and data DTOs cleanly?

    Keep two classes: a UserDto in the data layer that knows JSON, and a plain User entity in the domain that doesn't.

    // data layer: knows the wire formatclass UserDto {  const UserDto({required this.id, required this.fullName, this.avatarUrl});  final String id;  final String fullName;  final String? avatarUrl;…
  7. 07

    A reviewer rejects your domain entity because it imports package:flutter/material.dart for a Color field. Is that pedantic?

    Not pedantic: that one import reverses the dependency arrow.

    // domain: the meaning, no Flutter importenum OrderStatus { pending, paid, shipped, cancelled } // presentation: the look, chosen where Flutter already livesextension OrderStatusStyle on OrderStatus {  Color color(ColorScheme scheme) => switch (this) {…
  8. 08

    Your UserRepository has fourteen methods that mirror the REST routes one-for-one and returns Response objects. What would you change?

    That is an HTTP client wearing a repository's name.

    // before: an HTTP client wearing a repository's nameabstract class UserRepository {  Future<Response> getUserV2(String id);  Future<Response> postAvatarUpload(FormData data);  // ...twelve more, one per route}…
  9. 09

    Every rule about when an Order can be cancelled lives in the Cubit and Order is just a bag of fields. What does that cost you?

    That is an anemic domain model: the entity holds data only, so the rules that define what an order is get copied into every state holder that touches one.

    class Order {  Order({required this.id, required this.status, required this.placedAt, this.paidAt}) {    // invariant: an invalid Order cannot be constructed    if (status == OrderStatus.paid && paidAt == null) {      throw ArgumentError('A paid order needs paidAt');    }…
  10. 10

    QA sends a screenshot with a spinner and an error banner on screen together; the screen state is isLoading, error and items fields. What is wrong with the model?

    Three independent fields (isLoading, error, items) can hold eight combinations while the screen has four legal ones.

    sealed class OrdersState {  const OrdersState();}final class OrdersLoading extends OrdersState { const OrdersLoading(); }final class OrdersEmpty extends OrdersState { const OrdersEmpty(); }final class OrdersFailed extends OrdersState {…
  11. 11

    Which architecture rules does this Cubit break, and how would you restructure it?

    class ProfileCubit extends Cubit<ProfileState> {  ProfileCubit(this.context) : super(ProfileLoading());  final BuildContext context;   Future<void> load() async {    final res = await Dio().get('https://api.example.com/me');    final user = User.fromJson(res.data as Map<String, dynamic>);    emit(ProfileLoaded(user));    if (user.email == null) {      ScaffoldMessenger.of(context)          .showSnackBar(const SnackBar(content: Text('Add your email')));    }  }}

    It does three layers' jobs at once: it builds its own Dio, parses JSON, and keeps a BuildContext to show UI.

  12. 12

    Coming from Android, you create a ViewModel as below; it reloads on every rebuild and resets on a tab switch. Where does a ViewModel live in Flutter?

    class ProfileTab extends StatelessWidget {  const ProfileTab({super.key});   @override  Widget build(BuildContext context) {    final vm = ProfileViewModel(context.read<UserRepository>())..load();    return ListenableBuilder(      listenable: vm,      builder: (context, _) => Text(vm.name ?? 'Loading'),    );  }}

    A ViewModel lives in whatever owns it in the widget tree: a State or a provider element above the screens that use it, created once and disposed with that owner.

  13. 13

    You edit your name on the settings screen and the header still shows the old one until restart. Which layer do you fix, and how?

    Fix the repository, not the screens: each screen fetched its own copy, so there is no single source of truth.

    // BehaviorSubject comes from package:rxdartclass UserRepository {  UserRepository(this._api);  final UserApi _api;  // replays the last value to every late subscriber  final _user = BehaviorSubject<User>();…
  14. 14

    Find the bug: with no network, the orders screen says "You have no orders yet" instead of an error with a Retry button.

    class OrdersRepository {  OrdersRepository(this._api);  final OrdersApi _api;   Future<List<Order>> fetchOrders() async {    try {      final dtos = await _api.getOrders();      return dtos.map((dto) => dto.toEntity()).toList();    } catch (e, st) {      log('fetchOrders failed', error: e, stackTrace: st);      return [];    }  }}

    The repository turns every failure into an empty list, so the cubit cannot tell "no orders" from "no network": it shows the empty state, the Retry button never appears, and a 500 or a broken mapping looks exactly the same.

  15. 15

    How do you structure a large Flutter app (50+ screens, ten developers) so it stays maintainable?

    Group files by feature, not by layer: lib/features/cart/ holds the cart's models, repository, state holder and screens together, instead of scattering them across lib/models/, lib/blocs/ and lib/screens/.

    lib/  main.dart  app/                    # composition root, router, theme    app.dart    router.dart  core/                   # shared infrastructure, nothing feature-specific…
  16. 16

    How do you split a large Flutter app into modular packages or workspaces?

    Move each feature into its own local package under packages/ and let a thin app/ package depend on them.

    # pubspec.yaml at the repository root (pub workspaces, Dart 3.6+)name: shop_workspacepublish_to: noneenvironment:  sdk: ^3.6.0workspace:…
  17. 17

    How do you set up dependency injection at scale (composition root, scopes, environments)?

    Build the whole object graph in one place (the composition root, usually main.dart) and pass dependencies down through constructors.

    // main.dart: the composition root, the only file that names concrete typesvoid main() {  const env = String.fromEnvironment('ENV', defaultValue: 'prod');  // app scope: lives as long as the process  final api = ApiClient(baseUrl: env == 'prod' ? prodUrl : stagingUrl);  final AuthRepository auth = HttpAuthRepository(api);…
  18. 18

    Where in the layers do you catch, map and render errors, and when is a failure a Result instead of an exception?

    Catch transport exceptions once, in the repository, and rethrow them as a sealed class AppError; the view model keeps it in state and the UI switches over it.

    sealed class AppError implements Exception {  const AppError();}final class NetworkError extends AppError { const NetworkError(); }final class Unauthorized extends AppError { const Unauthorized(); }final class ServerError extends AppError {…
  19. 19

    How do feature flags and A/B tests fit into a Flutter app's architecture?

    A feature flag separates deploying code from releasing it: the code ships turned off, and you flip it on later for some users or for everyone.

  20. 20

    Signing out must clear the cart, close the chat socket and drop cached images. How do those features find out without importing each other?

    Don't let features import each other; that is the cycle nobody untangles later.

    abstract interface class SignOutHandler {  Future<void> onSignedOut();} // Lives in the composition root: the one file that knows every feature.class SessionCoordinator {…
  21. 21

    Placing an order reserves stock, charges the card and clears the cart. Which layer owns that sequence, and what happens when the charge fails?

    A use case owns the sequence: it spans three repositories, which is exactly what the domain layer is for.

    class PlaceOrder {  PlaceOrder(this._stock, this._payments, this._cart);  final StockRepository _stock;  final PaymentRepository _payments;  final CartRepository _cart;…
  22. 22

    You inherit a Flutter app with logic in StatefulWidgets and singletons everywhere. How do you improve it without a rewrite?

    Strangle it, don't rewrite it: freeze the old shape, build every new feature in the target shape, and migrate old code only when you already have a reason to open that file.