MVP & MVVM Architecture #
Every UI framework — from Android, iOS, Flutter, to web frontends — eventually faces the same question: where should presentation logic live? MVC gives the first answer: in the Controller. But the Controller in MVC often grows into a monster — it knows about HTTP, about the database, about business rules, and about display formatting all at once. MVP (Model–View–Presenter) was born to solve this problem with one key shift: the View becomes passive. The View no longer decides what is displayed — it only executes instructions from the Presenter through a contract (interface). MVVM (Model–View–ViewModel) goes a step further: the View automatically reacts to state changes in the ViewModel through data binding or reactive streams. MVVP (Model–View–View Presenter) is a variation that reasserts the Presenter’s role as an explicit manager of display state. These three patterns are responses to the same problem — how to separate presentation logic from the UI framework so it can be tested, reused, and developed independently.
The Evolution from MVC: Why MVC Is Not Enough for Complex UI #
flowchart LR
subgraph MVC_PROB["MVC — Problems in Complex UI"]
V1["View"] <-->|"direct update"| C1["Controller\\n(can get fat)"]
C1 <--> M1["Model"]
V1 <-->|"sometimes observes\\ndirectly"| M1
NOTE1["✗ View can depend on the Model\\n✗ Controller often becomes a God Object\\n✗ Hard to test without rendering the UI"]
end
subgraph MVP_SOL["MVP — Passive View"]
V2["View\\n(passive, only renders)"] <-->|"via interface"| P2["Presenter\\n(presentation logic)"]
P2 <--> M2["Model"]
NOTE2["✓ View does not know the Model\\n✓ Presenter can be tested without UI\\n✓ Thin, easily replaceable View"]
end
subgraph MVVM_SOL["MVVM — Reactive Binding"]
V3["View\\n(observes the ViewModel)"] -.->|"data binding"| VM3["ViewModel\\n(state + logic)"]
VM3 <--> M3["Model"]
NOTE3["✓ View updates automatically when state changes\\n✓ No View-to-Presenter coupling\\n✓ Purely testable ViewModel"]
endThe Three Patterns and Their Differences #
| Aspect | MVC | MVP | MVVM |
|---|---|---|---|
| Who controls the View | Controller | Presenter (via method calls) | ViewModel (via data binding / observe) |
| How passive is the View | Active — can go directly to the Model | Very passive — only renders | Very passive — only binds |
| How the View updates | Controller renders / redirects | Presenter calls View methods | State changes → View updates automatically |
| Testability | Moderate — tied to HTTP | Excellent — pure Presenter | Excellent — pure ViewModel |
| Best for | Server-side web | Android, iOS, Desktop | Flutter, React, SwiftUI, Jetpack Compose |
| Data binding | No | No | Yes — the heart of MVVM |
| Model observation | The View can directly | No — via the Presenter | Via ViewModel state |
MVP: Model–View–Presenter #
The View Contract as an Interface #
MVP’s main strength is a purely passive View — it makes no decisions at all. All decisions about what is displayed live in the Presenter. To enable this without direct coupling, the View defines an interface (contract) the Presenter uses to give instructions:
// mvp/article/view_contract.go — Contract between View and Presenter
package article
// ArticleListView is the interface implemented by the View
// ✓ The Presenter only knows this interface — not the UI implementation
type ArticleListView interface {
ShowArticles(articles []ArticleViewModel)
ShowError(message string)
ShowLoading()
HideLoading()
ShowEmptyState()
NavigateToDetail(articleID int64)
}
// ArticleDetailView is the contract for the article detail page
type ArticleDetailView interface {
ShowArticle(article ArticleDetailViewModel)
ShowError(message string)
ShowLoading()
HideLoading()
ShowPublishSuccess()
ShowPublishError(reason string)
}
// ArticleViewModel is the View Model — data already formatted for display
// ✓ Not a domain entity — only data ready for the View to display
type ArticleViewModel struct {
ID int64
Title string
Excerpt string // first 150 characters of the body
AuthorName string
PublishedAtText string // "2 days ago" instead of time.Time
IsPublished bool
}
type ArticleDetailViewModel struct {
ID int64
Title string
Body string
AuthorName string
PublishedAtText string
CanPublish bool // true if the user has permission
CanEdit bool
}
Presenter: Testable Presentation Logic #
// mvp/article/presenter.go — Presenter: all UI logic lives here
package article
import (
"context"
"fmt"
"time"
"myapp/domain"
"myapp/service"
)
// ArticleListPresenter manages the logic for the article list page
// ✓ No imports from any UI framework
// ✓ Can be tested with a mock View
type ArticleListPresenter struct {
view ArticleListView
articleSvc service.ArticleService
currentUser *domain.User
}
func NewArticleListPresenter(
view ArticleListView,
svc service.ArticleService,
user *domain.User,
) *ArticleListPresenter {
return &ArticleListPresenter{
view: view,
articleSvc: svc,
currentUser: user,
}
}
// LoadArticles is called when the View needs to display the article list
// ✓ The Presenter orchestrates loading, error, and success states
func (p *ArticleListPresenter) LoadArticles(ctx context.Context) {
p.view.ShowLoading()
articles, err := p.articleSvc.FindPublished(ctx)
p.view.HideLoading()
if err != nil {
p.view.ShowError("Failed to load articles: " + err.Error())
return
}
if len(articles) == 0 {
p.view.ShowEmptyState()
return
}
// Map domain objects to View Models — formatting happens in the Presenter
viewModels := make([]ArticleViewModel, len(articles))
for i, a := range articles {
viewModels[i] = ArticleViewModel{
ID: a.ID,
Title: a.Title,
Excerpt: truncate(a.Body, 150),
AuthorName: a.Author.FullName,
PublishedAtText: timeAgo(a.PublishedAt),
IsPublished: a.IsPublished(),
}
}
p.view.ShowArticles(viewModels)
}
// OnArticleTapped is called when the user taps an article
func (p *ArticleListPresenter) OnArticleTapped(articleID int64) {
p.view.NavigateToDetail(articleID)
}
// ArticleDetailPresenter manages the logic for the article detail page
type ArticleDetailPresenter struct {
view ArticleDetailView
articleSvc service.ArticleService
user *domain.User
articleID int64
}
func NewArticleDetailPresenter(
view ArticleDetailView,
svc service.ArticleService,
user *domain.User,
articleID int64,
) *ArticleDetailPresenter {
return &ArticleDetailPresenter{
view: view,
articleSvc: svc,
user: user,
articleID: articleID,
}
}
func (p *ArticleDetailPresenter) Load(ctx context.Context) {
p.view.ShowLoading()
article, err := p.articleSvc.FindByID(ctx, p.articleID)
p.view.HideLoading()
if err != nil {
p.view.ShowError("Article not found")
return
}
// Determine permissions — presentation logic lives in the Presenter
canPublish := !article.IsPublished() && p.user.HasRole("editor")
canEdit := p.user.HasRole("editor") || p.user.ID == article.AuthorID
p.view.ShowArticle(ArticleDetailViewModel{
ID: article.ID,
Title: article.Title,
Body: article.Body,
AuthorName: article.Author.FullName,
PublishedAtText: timeAgo(article.PublishedAt),
CanPublish: canPublish,
CanEdit: canEdit,
})
}
func (p *ArticleDetailPresenter) OnPublishTapped(ctx context.Context) {
p.view.ShowLoading()
_, err := p.articleSvc.Publish(ctx, p.articleID)
p.view.HideLoading()
if err != nil {
p.view.ShowPublishError(err.Error())
return
}
p.view.ShowPublishSuccess()
// Reload the article to update the display
p.Load(ctx)
}
// Helper functions — formatting logic lives in the Presenter, not the View
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
func timeAgo(t *time.Time) string {
if t == nil {
return "Not published"
}
diff := time.Since(*t)
switch {
case diff < time.Minute:
return "Just now"
case diff < time.Hour:
return fmt.Sprintf("%d minutes ago", int(diff.Minutes()))
case diff < 24*time.Hour:
return fmt.Sprintf("%d hours ago", int(diff.Hours()))
default:
return fmt.Sprintf("%d days ago", int(diff.Hours()/24))
}
}
Testing the Presenter Without UI #
This is MVP’s main strength — the Presenter can be fully tested without any UI framework:
// mvp/article/presenter_test.go
package article_test
import (
"context"
"testing"
"myapp/domain"
"myapp/mvp/article"
)
// MockArticleListView implements ArticleListView for testing
type MockArticleListView struct {
articles []article.ArticleViewModel
errorMessage string
isLoading bool
showedEmpty bool
}
func (m *MockArticleListView) ShowArticles(articles []article.ArticleViewModel) {
m.articles = articles
}
func (m *MockArticleListView) ShowError(msg string) { m.errorMessage = msg }
func (m *MockArticleListView) ShowLoading() { m.isLoading = true }
func (m *MockArticleListView) HideLoading() { m.isLoading = false }
func (m *MockArticleListView) ShowEmptyState() { m.showedEmpty = true }
func (m *MockArticleListView) NavigateToDetail(id int64) {}
// MockArticleService
type MockArticleService struct {
articles []*domain.Article
err error
}
func (m *MockArticleService) FindPublished(ctx context.Context) ([]*domain.Article, error) {
return m.articles, m.err
}
// ✓ This test needs no browser, no HTTP server, no database
func TestArticleListPresenter_LoadArticles_Success(t *testing.T) {
now := time.Now()
mockView := &MockArticleListView{}
mockSvc := &MockArticleService{
articles: []*domain.Article{
{ID: 1, Title: "First Article", Body: "Article body...", PublishedAt: &now},
{ID: 2, Title: "Second Article", Body: "Second article body...", PublishedAt: &now},
},
}
presenter := article.NewArticleListPresenter(mockView, mockSvc, &domain.User{})
presenter.LoadArticles(context.Background())
// Verify the View received the correct data
if len(mockView.articles) != 2 {
t.Errorf("expected 2 articles, got %d", len(mockView.articles))
}
if mockView.isLoading {
t.Error("loading must be hidden after completion")
}
if mockView.articles[0].Title != "First Article" {
t.Errorf("title does not match: %s", mockView.articles[0].Title)
}
}
func TestArticleListPresenter_LoadArticles_Empty(t *testing.T) {
mockView := &MockArticleListView{}
mockSvc := &MockArticleService{articles: nil}
presenter := article.NewArticleListPresenter(mockView, mockSvc, &domain.User{})
presenter.LoadArticles(context.Background())
if !mockView.showedEmpty {
t.Error("must show the empty state when there are no articles")
}
}
MVVM: Model–View–ViewModel #
MVVM differs from MVP in one fundamental way: the View does not imperatively call ViewModel methods — the View observes the ViewModel’s state and automatically updates the display when state changes. In Go, this can be implemented with channels or callbacks:
// mvvm/article/viewmodel.go — ViewModel: state + presentation logic
package article
import (
"context"
"sync"
"time"
)
// ArticleListState represents the entire UI state of the list page
// ✓ The ViewModel knows nothing about the UI framework — it only manages state
type ArticleListState struct {
IsLoading bool
Articles []ArticleViewModel
Error string
IsEmpty bool
}
// ArticleViewModel is data ready for the View to display
type ArticleViewModel struct {
ID int64
Title string
Excerpt string
AuthorName string
PublishedAtText string
}
// Observer is the function called when state changes
type Observer func(state ArticleListState)
// ArticleListViewModel manages state and notifies observers when it changes
type ArticleListViewModel struct {
mu sync.RWMutex
state ArticleListState
observers []Observer
articleSvc ArticleService
}
func NewArticleListViewModel(svc ArticleService) *ArticleListViewModel {
return &ArticleListViewModel{articleSvc: svc}
}
// Subscribe registers an observer — the View registers itself here
func (vm *ArticleListViewModel) Subscribe(observer Observer) {
vm.mu.Lock()
defer vm.mu.Unlock()
vm.observers = append(vm.observers, observer)
}
// notify tells all observers that the state has changed
func (vm *ArticleListViewModel) notify() {
vm.mu.RLock()
state := vm.state
observers := vm.observers
vm.mu.RUnlock()
for _, observer := range observers {
observer(state) // ✓ the View reacts automatically
}
}
// setState changes the state and immediately notifies observers
func (vm *ArticleListViewModel) setState(mutate func(*ArticleListState)) {
vm.mu.Lock()
mutate(&vm.state)
vm.mu.Unlock()
vm.notify()
}
// LoadArticles is an action the View can call
func (vm *ArticleListViewModel) LoadArticles(ctx context.Context) {
vm.setState(func(s *ArticleListState) {
s.IsLoading = true
s.Error = ""
})
articles, err := vm.articleSvc.FindPublished(ctx)
if err != nil {
vm.setState(func(s *ArticleListState) {
s.IsLoading = false
s.Error = "Failed to load articles"
})
return
}
viewModels := make([]ArticleViewModel, len(articles))
for i, a := range articles {
viewModels[i] = ArticleViewModel{
ID: a.ID,
Title: a.Title,
Excerpt: truncate(a.Body, 150),
AuthorName: a.AuthorName,
PublishedAtText: timeAgo(a.PublishedAt),
}
}
vm.setState(func(s *ArticleListState) {
s.IsLoading = false
s.Articles = viewModels
s.IsEmpty = len(viewModels) == 0
})
}
// CurrentState returns the current state — for initial synchronization
func (vm *ArticleListViewModel) CurrentState() ArticleListState {
vm.mu.RLock()
defer vm.mu.RUnlock()
return vm.state
}
MVVP: The Explicit View Presenter #
MVVP (Model–View–View Presenter) is an MVP variation that asserts the Presenter’s role more explicitly as the “display state manager”. Its difference from classic MVP is mainly terminological — the Presenter is called a View Presenter to emphasize that it is specifically responsible for UI state, not just a mediator.
flowchart TD
subgraph MVVP["MVVP — the View Presenter at the center"]
V["View\\n(passive, only renders\\nand sends events)"]
VP["View Presenter\\n• UI state\\n• data formatting\\n• navigation\\n• input validation\\n• loading/error states"]
M["Model\\n• domain entities\\n• business rules\\n• data access"]
end
U([User]) -->|"tap, input, gesture"| V
V -->|"onTapped(id)\\nonFormSubmit(data)"| VP
VP -->|"query / command"| M
M -->|"domain object"| VP
VP -->|"showX()\\nhideX()\\nnavigateToX()"| V
V -->|"render"| UThe key difference from regular MVP: the View Presenter explicitly manages UI state — it stores all the state needed to render the display correctly, including loading states, error states, pagination state, and filter state.
// mvvp/article/view_presenter.go — View Presenter with explicit UI state
package article
import (
"context"
"sync"
)
// UIState stores all state needed to render the View
type UIState struct {
IsLoading bool
IsRefreshing bool
CurrentPage int
TotalPages int
SearchQuery string
SortBy string
Articles []ArticleViewModel
ErrorMessage string
}
// ArticleListViewPresenter is the View Presenter for the article list page
// ✓ More stateful than a regular MVP Presenter
// ✓ Manages pagination, search, filters — all UI state lives here
type ArticleListViewPresenter struct {
mu sync.RWMutex
view ArticleListView
svc ArticleService
state UIState
}
func NewArticleListViewPresenter(view ArticleListView, svc ArticleService) *ArticleListViewPresenter {
return &ArticleListViewPresenter{
view: view,
svc: svc,
state: UIState{CurrentPage: 1, SortBy: "created_at"},
}
}
// OnSearchQueryChanged is called when the user changes the search query
func (p *ArticleListViewPresenter) OnSearchQueryChanged(ctx context.Context, query string) {
p.mu.Lock()
p.state.SearchQuery = query
p.state.CurrentPage = 1 // reset to the first page when searching
p.mu.Unlock()
p.loadArticles(ctx)
}
// OnSortChanged is called when the user changes the sort order
func (p *ArticleListViewPresenter) OnSortChanged(ctx context.Context, sortBy string) {
p.mu.Lock()
p.state.SortBy = sortBy
p.state.CurrentPage = 1
p.mu.Unlock()
p.loadArticles(ctx)
}
// OnNextPageTapped is called when the user presses the next page button
func (p *ArticleListViewPresenter) OnNextPageTapped(ctx context.Context) {
p.mu.RLock()
currentPage := p.state.CurrentPage
totalPages := p.state.TotalPages
p.mu.RUnlock()
if currentPage >= totalPages {
return // already on the last page
}
p.mu.Lock()
p.state.CurrentPage++
p.mu.Unlock()
p.loadArticles(ctx)
}
// OnRefresh is called when the user pull-to-refreshes
func (p *ArticleListViewPresenter) OnRefresh(ctx context.Context) {
p.mu.Lock()
p.state.IsRefreshing = true
p.state.CurrentPage = 1
p.mu.Unlock()
p.view.ShowRefreshing()
p.loadArticles(ctx)
}
func (p *ArticleListViewPresenter) loadArticles(ctx context.Context) {
p.mu.RLock()
query := p.state.SearchQuery
page := p.state.CurrentPage
sortBy := p.state.SortBy
p.mu.RUnlock()
p.view.ShowLoading()
result, err := p.svc.Search(ctx, SearchParams{
Query: query,
Page: page,
SortBy: sortBy,
})
p.view.HideLoading()
p.view.HideRefreshing()
if err != nil {
p.mu.Lock()
p.state.ErrorMessage = err.Error()
p.state.IsRefreshing = false
p.mu.Unlock()
p.view.ShowError(err.Error())
return
}
p.mu.Lock()
p.state.Articles = toViewModels(result.Articles)
p.state.TotalPages = result.TotalPages
p.state.ErrorMessage = ""
p.state.IsRefreshing = false
p.mu.Unlock()
p.view.ShowArticles(p.state.Articles)
p.view.UpdatePagination(page, result.TotalPages)
}
// GetCurrentState returns a snapshot of the current state
func (p *ArticleListViewPresenter) GetCurrentState() UIState {
p.mu.RLock()
defer p.mu.RUnlock()
return p.state
}
Full Comparison: MVC, MVP, MVVM, MVVP #
flowchart TD
subgraph ALL["The Presentation Pattern Family"]
MVC["MVC\\nController as mediator\\nView can go directly to the Model"]
MVP["MVP\\nPresenter as mediator\\nPassive View via interface"]
MVVM["MVVM\\nViewModel as state\\nReactive View via binding"]
MVVP["MVVP\\nView Presenter with explicit UI state\\nA more stateful evolution of MVP"]
end
MVC -->|"View too active\\nController too fat"| MVP
MVP -->|"imperative still there\\nwant fully reactive"| MVVM
MVP -->|"need more structured\\nUI state"| MVVP| Criterion | MVC | MVP | MVVM | MVVP |
|---|---|---|---|---|
| Testability | Moderate | High | High | High |
| View passivity | Low | Very high | Very high | Very high |
| State management | In the Controller | In the Presenter | In the ViewModel | In the View Presenter |
| Update mechanism | Imperative | Imperative | Reactive / binding | Imperative + stateful |
| Boilerplate | Low | Moderate | High (binding) | Moderate–high |
| Best for | Server-side web | Android, iOS, Desktop | Flutter, React, Compose | Complex UI, wizards, workflows |
Anti-Patterns to Avoid #
// ✗ God Presenter — a Presenter that is too large, knowing about too many things
type ArticlePresenter struct {
// ✗ The Presenter holds dependencies to every service
articleSvc ArticleService
userSvc UserService
commentSvc CommentService
analyticsEv AnalyticsEventTracker
paymentSvc PaymentService
// This is a sign the Presenter needs to be split
}
// ✓ Split into several smaller Presenters
type ArticleListPresenter struct { articleSvc ArticleService }
type ArticleDetailPresenter struct { articleSvc ArticleService; commentSvc CommentService }
type ArticleAnalyticsPresenter struct { analyticsEv AnalyticsEventTracker }
// ✗ The View making decisions on its own — not passive
// In Android/mobile
class ArticleListView {
fun showArticles(articles: List<Article>) {
// ✗ the View formats data itself
articles.forEach { article ->
val text = if (article.isPublished) "✓ ${article.title}" else article.title
// ...
}
}
}
// ✓ The Presenter formats — the View only renders what is ready
class ArticleListPresenter {
fun loadArticles() {
val articles = service.findPublished()
val viewModels = articles.map { article ->
ArticleViewModel(
displayTitle = if (article.isPublished) "✓ ${article.title}" else article.title
)
}
view.showArticles(viewModels) // ✓ the view only renders ready data
}
}
// ✗ The Presenter importing a UI framework — hard to test
// ✗ import android.view.View — the Presenter depends on the Android framework
// ✗ import UIKit — the Presenter depends on the iOS framework
// ✓ The Presenter only depends on interfaces (View Contracts)
// Interfaces do not depend on any UI framework
// The Presenter can be tested with mocks implementing the interface
// ✗ Business logic in the Presenter
type ArticlePresenter struct{}
func (p *ArticlePresenter) OnPublishTapped(ctx context.Context, articleID int64) {
article, _ := p.svc.FindByID(ctx, articleID)
// ✗ Business rule in the Presenter — should be in the domain/service
if len(article.Body) < 100 {
p.view.ShowError("article is too short to publish")
return
}
if article.AuthorID != p.currentUser.ID {
p.view.ShowError("you are not the author of this article")
return
}
// ...
}
// ✓ Business logic in the domain/service — the Presenter only coordinates
func (p *ArticlePresenter) OnPublishTapped(ctx context.Context, articleID int64) {
_, err := p.articleSvc.Publish(ctx, articleID) // ✓ business rule in the service
if err != nil {
p.view.ShowPublishError(err.Error())
return
}
p.view.ShowPublishSuccess()
}
When to Choose Which Pattern #
Use MVC if:
✓ Server-side web rendering (using HTML templates)
✓ Simple backend APIs
✓ Teams familiar with MVC that do not need high UI testability
Use MVP if:
✓ Mobile applications (Android, iOS) with significant UI interaction
✓ Desktop applications
✓ Presenter unit tests are needed without running the UI
✓ The View needs to be replaceable (e.g., web view vs native view)
Use MVVM if:
✓ The framework supports data binding (Flutter, React, Jetpack Compose, SwiftUI)
✓ Highly reactive UI with lots of interrelated state
✓ Teams familiar with reactive programming
Use MVVP if:
✓ Explicit, structured UI state is needed (pagination, search, filters)
✓ The UI is a wizard or multi-step workflow
✓ More detailed control than MVP is needed but MVVM data binding is not wanted
MVP/MVVM/MVVP Review Checklist #
VIEW:
□ The View makes no decisions — only renders and forwards events
□ The View does not access services or repositories directly
□ The View only calls Presenter/ViewModel methods, never processes data
□ The View can be replaced with another implementation without changing the Presenter
PRESENTER / VIEWMODEL:
□ No imports from UI frameworks (Android, iOS, Flutter, HTML)
□ All business logic lives in services/domain, not the Presenter
□ View Models (data for the View) differ from domain entities
□ Data formatting (dates, numbers, text) happens in the Presenter, not the View
□ The Presenter can be tested with a mock View without running the UI
MODEL:
□ Domain entities know nothing about the Presenter or View
□ Business rules live in the model/service, not the Presenter
□ Repository interfaces live in the model layer
VIEW CONTRACT (INTERFACE):
□ Interfaces are defined from the Presenter's needs perspective
□ Interface methods use UI language ("showLoading", "showError")
□ Interfaces do not depend on any specific UI framework
TESTING:
□ Presenters/ViewModels are tested with mock Views
□ Tests verify the correct View methods are called
□ Tests cover loading, error, empty, and success scenarios
Summary #
- MVP, MVVM, and MVVP all address the same MVC problem — a View that is too active and a Controller/Presenter that is too fat; all three force the View to be passive.
- The key to MVP is the View Contract (interface) — the Presenter only knows the View interface, not its implementation; this lets the Presenter be tested with mocks without running the UI.
- Presenters must not depend on UI frameworks — no Android, iOS, or Flutter imports in the Presenter; depending on a UI framework makes the Presenter hard to test.
- View Models differ from domain entities — create ViewModel/View Model structs specific to the display (date formatting, pre-formatted text, permission flags); never expose domain entities directly to the View.
- Formatting is the Presenter’s responsibility —
"2 days ago"instead oftime.Time,"Rp 150,000"instead ofint64; the View only renders ready-made strings.- MVVM adds reactivity — state changes in the ViewModel and the View updates automatically; fits frameworks that support data binding.
- MVVP is more explicit about UI state — the View Presenter stores all display state (pagination, search, sort) in a structured way; more suitable for complex UI.
- The God Presenter is the main anti-pattern — if a Presenter has more than 3–4 dependencies or more than 300 lines, it is a sign the Presenter needs splitting.
- Business logic in the Presenter is a violation — business rules (“articles must be at least 100 characters”) live in the domain or service, not the Presenter; the Presenter only orchestrates presentation.
- All three are presentation patterns, not full architectures — MVP/MVVM/MVVP answer how the UI is organized; they are often combined with Clean Architecture or Layered Architecture for the deeper layers.
← Previous: MVC