Explain mobile app architecture patterns: MVC, MVP, MVVM, and Clean Architecture.

📱 App Development9/21/2025
Understanding different architectural patterns for mobile app development, their benefits, drawbacks, and implementation approaches.

Mobile App Architecture Patterns

1. MVC (Model-View-Controller)

Structure

User Input → [Controller] → [Model]
                ↓             ↓
            [View] ←----------+

Components

  • Model: Data and business logic
  • View: UI representation
  • Controller: Handles user input, updates model and view

iOS Implementation

// Model
class User {
    var name: String
    var email: String
    
    init(name: String, email: String) {
        self.name = name
        self.email = email
    }
}

// Controller
class UserViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!
    
    var user: User?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }
    
    func updateUI() {
        nameLabel.text = user?.name
        emailLabel.text = user?.email
    }
    
    @IBAction func editButtonTapped(_ sender: Any) {
        // Handle user input
        user?.name = "Updated Name"
        updateUI()
    }
}

Pros & Cons

Pros:

  • Simple and familiar pattern
  • Good separation of concerns
  • Easy to understand

Cons:

  • Tight coupling between View and Controller
  • Controllers can become massive (Massive View Controller problem)
  • Difficult to unit test UI logic

2. MVP (Model-View-Presenter)

Structure

[View] ←→ [Presenter] ←→ [Model]

Components

  • Model: Data and business logic
  • View: Passive UI (no business logic)
  • Presenter: Handles UI logic and coordinates between View and Model

Android Implementation

// Contract interface
public interface UserContract {
    interface View {
        void showUser(User user);
        void showError(String error);
        void showLoading();
        void hideLoading();
    }
    
    interface Presenter {
        void loadUser(String userId);
        void onDestroy();
    }
}

// Presenter
public class UserPresenter implements UserContract.Presenter {
    private UserContract.View view;
    private UserRepository repository;
    
    public UserPresenter(UserContract.View view, UserRepository repository) {
        this.view = view;
        this.repository = repository;
    }
    
    @Override
    public void loadUser(String userId) {
        view.showLoading();
        repository.getUser(userId, new Callback<User>() {
            @Override
            public void onSuccess(User user) {
                view.hideLoading();
                view.showUser(user);
            }
            
            @Override
            public void onError(String error) {
                view.hideLoading();
                view.showError(error);
            }
        });
    }
}

// View (Activity/Fragment)
public class UserActivity extends AppCompatActivity implements UserContract.View {
    private UserContract.Presenter presenter;
    private TextView nameTextView;
    private ProgressBar loadingView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);
        
        presenter = new UserPresenter(this, new UserRepository());
        presenter.loadUser("123");
    }
    
    @Override
    public void showUser(User user) {
        nameTextView.setText(user.getName());
    }
    
    @Override
    public void showError(String error) {
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
    }
}

Pros & Cons

Pros:

  • Better separation of concerns than MVC
  • Easier to unit test (View is passive)
  • Presenter can be reused

Cons:

  • More boilerplate code
  • Presenter can become complex
  • Memory leaks if not handled properly

3. MVVM (Model-View-ViewModel)

Structure

[View] ←→ [ViewModel] ←→ [Model]
   ↑           ↓
   ←-- Binding --→

Components

  • Model: Data and business logic
  • View: UI layer
  • ViewModel: Exposes data to View through data binding

Flutter Implementation

// Model
class User {
  final String name;
  final String email;
  
  User({required this.name, required this.email});
}

// ViewModel
class UserViewModel extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;
  String? _error;
  
  User? get user => _user;
  bool get isLoading => _isLoading;
  String? get error => _error;
  
  final UserRepository _repository;
  
  UserViewModel(this._repository);
  
  Future<void> loadUser(String userId) async {
    _isLoading = true;
    _error = null;
    notifyListeners();
    
    try {
      _user = await _repository.getUser(userId);
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// View
class UserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Consumer<UserViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) {
            return CircularProgressIndicator();
          }
          
          if (viewModel.error != null) {
            return Text('Error: ${viewModel.error}');
          }
          
          return Column(
            children: [
              Text('Name: ${viewModel.user?.name}'),
              Text('Email: ${viewModel.user?.email}'),
            ],
          );
        },
      ),
    );
  }
}

iOS SwiftUI Implementation

// ViewModel
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var error: String?
    
    private let repository: UserRepository
    
    init(repository: UserRepository) {
        self.repository = repository
    }
    
    func loadUser(id: String) {
        isLoading = true
        error = nil
        
        repository.getUser(id: id) { [weak self] result in
            DispatchQueue.main.async {
                self?.isLoading = false
                
                switch result {
                case .success(let user):
                    self?.user = user
                case .failure(let error):
                    self?.error = error.localizedDescription
                }
            }
        }
    }
}

// View
struct UserView: View {
    @StateObject private var viewModel = UserViewModel(repository: UserRepository())
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else if let error = viewModel.error {
                Text("Error: \(error)")
            } else if let user = viewModel.user {
                Text("Name: \(user.name)")
                Text("Email: \(user.email)")
            }
        }
        .onAppear {
            viewModel.loadUser(id: "123")
        }
    }
}

Pros & Cons

Pros:

  • Great for data binding
  • Highly testable
  • Loose coupling between View and ViewModel
  • Supports reactive programming

Cons:

  • Learning curve for data binding
  • Can be overkill for simple apps
  • Memory management considerations

4. Clean Architecture

Structure

    [Presentation Layer]
           ↓
    [Domain Layer]
           ↓
    [Data Layer]

Layers

Domain Layer (Business Logic)
// Entity
class User {
  final String id;
  final String name;
  final String email;
  
  User({required this.id, required this.name, required this.email});
}

// Repository Interface
abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> saveUser(User user);
}

// Use Case
class GetUserUseCase {
  final UserRepository repository;
  
  GetUserUseCase(this.repository);
  
  Future<User> execute(String userId) {
    return repository.getUser(userId);
  }
}
Data Layer
// Data Source
class UserRemoteDataSource {
  final ApiClient apiClient;
  
  UserRemoteDataSource(this.apiClient);
  
  Future<UserModel> getUser(String id) async {
    final response = await apiClient.get('/users/$id');
    return UserModel.fromJson(response.data);
  }
}

// Repository Implementation
class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remoteDataSource;
  final UserLocalDataSource localDataSource;
  
  UserRepositoryImpl(this.remoteDataSource, this.localDataSource);
  
  @Override
  Future<User> getUser(String id) async {
    try {
      final userModel = await remoteDataSource.getUser(id);
      await localDataSource.cacheUser(userModel);
      return userModel.toEntity();
    } catch (e) {
      final cachedUser = await localDataSource.getUser(id);
      return cachedUser.toEntity();
    }
  }
}
Presentation Layer
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserUseCase getUserUseCase;
  
  UserBloc(this.getUserUseCase) : super(UserInitial()) {
    on<LoadUser>((event, emit) async {
      emit(UserLoading());
      try {
        final user = await getUserUseCase.execute(event.userId);
        emit(UserLoaded(user));
      } catch (e) {
        emit(UserError(e.toString()));
      }
    });
  }
}

Dependency Injection

// Service Locator
class ServiceLocator {
  static final GetIt _instance = GetIt.instance;
  
  static void setup() {
    // Data Layer
    _instance.registerLazySingleton<ApiClient>(() => ApiClient());
    _instance.registerLazySingleton<UserRemoteDataSource>(
      () => UserRemoteDataSource(_instance())
    );
    
    // Domain Layer
    _instance.registerLazySingleton<UserRepository>(
      () => UserRepositoryImpl(_instance(), _instance())
    );
    _instance.registerLazySingleton<GetUserUseCase>(
      () => GetUserUseCase(_instance())
    );
    
    // Presentation Layer
    _instance.registerFactory<UserBloc>(
      () => UserBloc(_instance())
    );
  }
}

Architecture Comparison

PatternComplexityTestabilityMaintainabilityLearning Curve
MVCLowLowMediumEasy
MVPMediumHighHighMedium
MVVMMediumHighHighMedium
Clean ArchitectureHighVery HighVery HighSteep

Choosing the Right Architecture

Small Apps (< 10 screens)

  • MVC or MVP - Simple and sufficient
  • Focus on rapid development
  • Less overhead

Medium Apps (10-50 screens)

  • MVVM - Good balance of simplicity and structure
  • Supports data binding
  • Easier testing than MVC

Large Apps (50+ screens)

  • Clean Architecture - Maximum maintainability
  • Multiple developers/teams
  • Long-term project
  • Complex business logic

Team Considerations

  • Junior developers: Start with MVC/MVP
  • Mixed experience: MVVM with good documentation
  • Senior developers: Clean Architecture for complex projects
By: System Admin