Explain mobile app architecture patterns: MVC, MVP, MVVM, and Clean Architecture.
📱 App Development• 9/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
Pattern | Complexity | Testability | Maintainability | Learning Curve |
---|---|---|---|---|
MVC | Low | Low | Medium | Easy |
MVP | Medium | High | High | Medium |
MVVM | Medium | High | High | Medium |
Clean Architecture | High | Very High | Very High | Steep |
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