MVP & MVVM Architecture #

Setiap framework UI — dari Android, iOS, Flutter, hingga web frontend — pada akhirnya menghadapi pertanyaan yang sama: di mana logika presentasi seharusnya tinggal? MVC memberi jawaban pertama: di Controller. Tapi Controller di MVC sering tumbuh menjadi monster — ia tahu tentang HTTP, tentang database, tentang business rules, dan tentang format tampilan sekaligus. MVP (Model–View–Presenter) lahir untuk memecahkan masalah ini dengan satu pergeseran kunci: View menjadi pasif. View tidak lagi memutuskan apa yang ditampilkan — ia hanya mengeksekusi instruksi dari Presenter melalui sebuah kontrak (interface). MVVM (Model–View–ViewModel) mengambil langkah lebih jauh: View secara otomatis bereaksi terhadap perubahan state di ViewModel melalui data binding atau reactive streams. MVVP (Model–View–View Presenter) adalah variasi yang menegaskan kembali peran Presenter sebagai pengelola state tampilan secara eksplisit. Ketiga pola ini adalah respons terhadap masalah yang sama — bagaimana memisahkan logika presentasi dari framework UI sehingga bisa diuji, di-reuse, dan dikembangkan secara mandiri.

Evolusi dari MVC: Mengapa MVC Tidak Cukup untuk UI Kompleks #

flowchart LR
    subgraph MVC_PROB["MVC — Masalah di UI Kompleks"]
        V1["View"] <-->|"langsung update"| C1["Controller\n(bisa gemuk)"]
        C1 <--> M1["Model"]
        V1 <-->|"kadang langsung\nobservasi"| M1
        NOTE1["✗ View bisa bergantung ke Model\n✗ Controller sering jadi God Object\n✗ Sulit ditest tanpa render UI"]
    end

    subgraph MVP_SOL["MVP — Passive View"]
        V2["View\n(pasif, hanya render)"] <-->|"via interface"| P2["Presenter\n(logika presentasi)"]
        P2 <--> M2["Model"]
        NOTE2["✓ View tidak tahu Model\n✓ Presenter bisa ditest tanpa UI\n✓ View tipis dan mudah diganti"]
    end

    subgraph MVVM_SOL["MVVM — Reactive Binding"]
        V3["View\n(observe ViewModel)"] -.->|"data binding"| VM3["ViewModel\n(state + logic)"]
        VM3 <--> M3["Model"]
        NOTE3["✓ View otomatis update saat state berubah\n✓ Tidak ada coupling View ke Presenter\n✓ ViewModel murni testable"]
    end

Tiga Pola dan Perbedaannya #

Aspek MVC MVP MVVM
Siapa yang mengontrol View Controller Presenter (via method call) ViewModel (via data binding / observe)
View seberapa pasif Aktif — bisa langsung ke Model Sangat pasif — hanya render Sangat pasif — hanya bind
Cara update View Controller render / redirect Presenter memanggil method View State berubah → View otomatis update
Testability Sedang — terkait HTTP Sangat baik — Presenter murni Sangat baik — ViewModel murni
Cocok untuk Web server-side Android, iOS, Desktop Flutter, React, SwiftUI, Jetpack Compose
Data binding Tidak Tidak Ya — jantung dari MVVM
Observasi Model View bisa langsung Tidak — via Presenter Via ViewModel state

MVP: Model–View–Presenter #

Kontrak View sebagai Interface #

Kekuatan utama MVP adalah View yang murni pasif — ia tidak membuat keputusan apapun. Semua keputusan tentang apa yang ditampilkan ada di Presenter. Untuk memungkinkan ini tanpa coupling langsung, View mendefinisikan interface (kontrak) yang Presenter gunakan untuk memberi instruksi:

// mvp/article/view_contract.go — Kontrak antara View dan Presenter
package article

// ArticleListView adalah interface yang diimplementasikan oleh View
// ✓ Presenter hanya tahu tentang interface ini — tidak tahu implementasi UI-nya
type ArticleListView interface {
	ShowArticles(articles []ArticleViewModel)
	ShowError(message string)
	ShowLoading()
	HideLoading()
	ShowEmptyState()
	NavigateToDetail(articleID int64)
}

// ArticleDetailView adalah kontrak untuk halaman detail artikel
type ArticleDetailView interface {
	ShowArticle(article ArticleDetailViewModel)
	ShowError(message string)
	ShowLoading()
	HideLoading()
	ShowPublishSuccess()
	ShowPublishError(reason string)
}

// ArticleViewModel adalah View Model — data yang sudah diformat untuk tampilan
// ✓ Bukan domain entity — hanya data yang siap ditampilkan View
type ArticleViewModel struct {
	ID              int64
	Title           string
	Excerpt         string     // 150 karakter pertama body
	AuthorName      string
	PublishedAtText string     // "2 hari lalu" bukan time.Time
	IsPublished     bool
}

type ArticleDetailViewModel struct {
	ID              int64
	Title           string
	Body            string
	AuthorName      string
	PublishedAtText string
	CanPublish      bool      // true jika user punya permission
	CanEdit         bool
}

Presenter: Logika Presentasi yang Testable #

// mvp/article/presenter.go — Presenter: semua logika UI ada di sini
package article

import (
	"context"
	"fmt"
	"time"

	"myapp/domain"
	"myapp/service"
)

// ArticleListPresenter mengelola logika untuk halaman daftar artikel
// ✓ Tidak ada import dari framework UI apapun
// ✓ Bisa ditest dengan 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 dipanggil ketika View perlu menampilkan daftar artikel
// ✓ Presenter mengorkestrasi loading, error, dan success state
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("Gagal memuat artikel: " + err.Error())
		return
	}

	if len(articles) == 0 {
		p.view.ShowEmptyState()
		return
	}

	// Map domain object ke View Model — formatting dilakukan di 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 dipanggil ketika user mengetuk satu artikel
func (p *ArticleListPresenter) OnArticleTapped(articleID int64) {
	p.view.NavigateToDetail(articleID)
}

// ArticleDetailPresenter mengelola logika untuk halaman detail artikel
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("Artikel tidak ditemukan")
		return
	}

	// Tentukan permission — logika presentasi ada di 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 artikel untuk memperbarui tampilan
	p.Load(ctx)
}

// Helper functions — formatting logic ada di Presenter, bukan di 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 "Belum diterbitkan"
	}
	diff := time.Since(*t)
	switch {
	case diff < time.Minute:
		return "Baru saja"
	case diff < time.Hour:
		return fmt.Sprintf("%d menit lalu", int(diff.Minutes()))
	case diff < 24*time.Hour:
		return fmt.Sprintf("%d jam lalu", int(diff.Hours()))
	default:
		return fmt.Sprintf("%d hari lalu", int(diff.Hours()/24))
	}
}

Pengujian Presenter Tanpa UI #

Inilah kekuatan utama MVP — Presenter bisa diuji sepenuhnya tanpa framework UI:

// mvp/article/presenter_test.go
package article_test

import (
	"context"
	"testing"

	"myapp/domain"
	"myapp/mvp/article"
)

// MockArticleListView mengimplementasikan ArticleListView untuk 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
}

// ✓ Test ini tidak butuh browser, tidak butuh HTTP server, tidak butuh database
func TestArticleListPresenter_LoadArticles_Success(t *testing.T) {
	now := time.Now()
	mockView := &MockArticleListView{}
	mockSvc := &MockArticleService{
		articles: []*domain.Article{
			{ID: 1, Title: "Artikel Pertama", Body: "Isi artikel...", PublishedAt: &now},
			{ID: 2, Title: "Artikel Kedua", Body: "Isi artikel dua...", PublishedAt: &now},
		},
	}

	presenter := article.NewArticleListPresenter(mockView, mockSvc, &domain.User{})
	presenter.LoadArticles(context.Background())

	// Verifikasi bahwa View menerima data yang benar
	if len(mockView.articles) != 2 {
		t.Errorf("expected 2 articles, got %d", len(mockView.articles))
	}
	if mockView.isLoading {
		t.Error("loading harus disembunyikan setelah selesai")
	}
	if mockView.articles[0].Title != "Artikel Pertama" {
		t.Errorf("judul tidak sesuai: %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("harus menampilkan empty state saat tidak ada artikel")
	}
}

MVVM: Model–View–ViewModel #

MVVM berbeda dari MVP dalam satu hal fundamental: View tidak memanggil method ViewModel secara imperatif — View mengamati state ViewModel dan secara otomatis memperbarui tampilan saat state berubah. Di Go, ini bisa diimplementasikan dengan channel atau callback:

// mvvm/article/viewmodel.go — ViewModel: state + logika presentasi
package article

import (
	"context"
	"sync"
	"time"
)

// ArticleListState merepresentasikan seluruh state UI halaman daftar
// ✓ ViewModel tidak tahu tentang framework UI — hanya mengelola state
type ArticleListState struct {
	IsLoading bool
	Articles  []ArticleViewModel
	Error     string
	IsEmpty   bool
}

// ArticleViewModel adalah data yang siap ditampilkan View
type ArticleViewModel struct {
	ID              int64
	Title           string
	Excerpt         string
	AuthorName      string
	PublishedAtText string
}

// Observer adalah fungsi yang dipanggil saat state berubah
type Observer func(state ArticleListState)

// ArticleListViewModel mengelola state dan menotifikasi observer saat berubah
type ArticleListViewModel struct {
	mu         sync.RWMutex
	state      ArticleListState
	observers  []Observer
	articleSvc ArticleService
}

func NewArticleListViewModel(svc ArticleService) *ArticleListViewModel {
	return &ArticleListViewModel{articleSvc: svc}
}

// Subscribe mendaftarkan observer — View mendaftarkan diri di sini
func (vm *ArticleListViewModel) Subscribe(observer Observer) {
	vm.mu.Lock()
	defer vm.mu.Unlock()
	vm.observers = append(vm.observers, observer)
}

// notify memberitahu semua observer bahwa state telah berubah
func (vm *ArticleListViewModel) notify() {
	vm.mu.RLock()
	state := vm.state
	observers := vm.observers
	vm.mu.RUnlock()

	for _, observer := range observers {
		observer(state) // ✓ View bereaksi secara otomatis
	}
}

// setState mengubah state dan langsung menotifikasi observer
func (vm *ArticleListViewModel) setState(mutate func(*ArticleListState)) {
	vm.mu.Lock()
	mutate(&vm.state)
	vm.mu.Unlock()
	vm.notify()
}

// LoadArticles adalah action yang bisa dipanggil View
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 = "Gagal memuat artikel"
		})
		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 mengembalikan state saat ini — untuk sinkronisasi awal
func (vm *ArticleListViewModel) CurrentState() ArticleListState {
	vm.mu.RLock()
	defer vm.mu.RUnlock()
	return vm.state
}

MVVP: View Presenter yang Eksplisit #

MVVP (Model–View–View Presenter) adalah variasi MVP yang menegaskan peran Presenter lebih eksplisit sebagai “pengelola state tampilan”. Perbedaannya dengan MVP klasik terutama terminologis — Presenter disebut View Presenter untuk menekankan bahwa ia secara spesifik bertanggung jawab atas state UI, bukan hanya sebagai mediator.

flowchart TD
    subgraph MVVP["MVVP — View Presenter sebagai pusat"]
        V["View\n(pasif, hanya render\ndan kirim event)"]
        VP["View Presenter\n• state UI\n• formatting data\n• navigasi\n• validasi input\n• loading/error state"]
        M["Model\n• domain entity\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"| U

Perbedaan kunci dari MVP biasa: View Presenter secara eksplisit mengelola state UI — ia menyimpan semua state yang diperlukan untuk merender tampilan dengan benar, termasuk loading state, error state, pagination state, dan filter state.

// mvvp/article/view_presenter.go — View Presenter dengan state UI eksplisit
package article

import (
	"context"
	"sync"
)

// UIState menyimpan semua state yang dibutuhkan untuk merender View
type UIState struct {
	IsLoading    bool
	IsRefreshing bool
	CurrentPage  int
	TotalPages   int
	SearchQuery  string
	SortBy       string
	Articles     []ArticleViewModel
	ErrorMessage string
}

// ArticleListViewPresenter adalah View Presenter untuk halaman daftar artikel
// ✓ Lebih stateful dari MVP Presenter biasa
// ✓ Mengelola pagination, search, filter — semua state UI ada di sini
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 dipanggil saat user mengubah query pencarian
func (p *ArticleListViewPresenter) OnSearchQueryChanged(ctx context.Context, query string) {
	p.mu.Lock()
	p.state.SearchQuery = query
	p.state.CurrentPage = 1 // reset ke halaman pertama saat search
	p.mu.Unlock()

	p.loadArticles(ctx)
}

// OnSortChanged dipanggil saat user mengubah urutan tampilan
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 dipanggil saat user menekan tombol halaman berikutnya
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 // sudah di halaman terakhir
	}

	p.mu.Lock()
	p.state.CurrentPage++
	p.mu.Unlock()

	p.loadArticles(ctx)
}

// OnRefresh dipanggil saat user pull-to-refresh
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 mengembalikan snapshot state saat ini
func (p *ArticleListViewPresenter) GetCurrentState() UIState {
	p.mu.RLock()
	defer p.mu.RUnlock()
	return p.state
}

Perbandingan Lengkap: MVC, MVP, MVVM, MVVP #

flowchart TD
    subgraph ALL["Keluarga Presentation Pattern"]
        MVC["MVC\nController sebagai mediator\nView bisa langsung ke Model"]
        MVP["MVP\nPresenter sebagai mediator\nView pasif via interface"]
        MVVM["MVVM\nViewModel sebagai state\nView reactive via binding"]
        MVVP["MVVP\nView Presenter dengan state UI eksplisit\nEvolusi MVP yang lebih stateful"]
    end

    MVC -->|"View terlalu aktif\nController gemuk"| MVP
    MVP -->|"imperatif masih ada\ningin fully reactive"| MVVM
    MVP -->|"perlu state UI\nlebih terstruktur"| MVVP
Kriteria MVC MVP MVVM MVVP
Testability Sedang Tinggi Tinggi Tinggi
View passivity Rendah Sangat tinggi Sangat tinggi Sangat tinggi
State management Di Controller Di Presenter Di ViewModel Di View Presenter
Update mekanisme Imperatif Imperatif Reaktif / binding Imperatif + stateful
Boilerplate Rendah Sedang Tinggi (binding) Sedang-tinggi
Cocok untuk Web server-side Android, iOS, Desktop Flutter, React, Compose UI kompleks, wizard, workflow

Anti-Pattern yang Harus Dihindari #

// ✗ God Presenter — Presenter terlalu besar, tahu tentang terlalu banyak hal
type ArticlePresenter struct {
	// ✗ Presenter menyimpan dependency ke semua service
	articleSvc  ArticleService
	userSvc     UserService
	commentSvc  CommentService
	analyticsEv AnalyticsEventTracker
	paymentSvc  PaymentService
	// Ini adalah tanda bahwa Presenter perlu dipecah
}

// ✓ Pisahkan ke beberapa Presenter yang lebih kecil
type ArticleListPresenter struct { articleSvc ArticleService }
type ArticleDetailPresenter struct { articleSvc ArticleService; commentSvc CommentService }
type ArticleAnalyticsPresenter struct { analyticsEv AnalyticsEventTracker }

// ✗ View mengambil keputusan sendiri — tidak pasif
// Di Android/mobile
class ArticleListView {
    fun showArticles(articles: List<Article>) {
        // ✗ View memformat data sendiri
        articles.forEach { article ->
            val text = if (article.isPublished) "✓ ${article.title}" else article.title
            // ...
        }
    }
}

// ✓ Presenter yang memformat — View hanya render apa yang sudah siap
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) // ✓ view hanya render data yang sudah siap
    }
}

// ✗ Presenter mengimport framework UI — sulit ditest
// ✗ import android.view.View — Presenter bergantung ke Android framework
// ✗ import UIKit — Presenter bergantung ke iOS framework

// ✓ Presenter hanya bergantung pada interface (View Contract)
// Interface tidak bergantung pada framework UI apapun
// Presenter bisa ditest dengan mock yang mengimplementasikan interface

// ✗ Business logic di Presenter
type ArticlePresenter struct{}

func (p *ArticlePresenter) OnPublishTapped(ctx context.Context, articleID int64) {
	article, _ := p.svc.FindByID(ctx, articleID)
	// ✗ Business rule di Presenter — seharusnya di domain/service
	if len(article.Body) < 100 {
		p.view.ShowError("artikel terlalu pendek untuk diterbitkan")
		return
	}
	if article.AuthorID != p.currentUser.ID {
		p.view.ShowError("kamu bukan penulis artikel ini")
		return
	}
	// ...
}

// ✓ Business logic di domain/service — Presenter hanya koordinasi
func (p *ArticlePresenter) OnPublishTapped(ctx context.Context, articleID int64) {
	_, err := p.articleSvc.Publish(ctx, articleID) // ✓ business rule di service
	if err != nil {
		p.view.ShowPublishError(err.Error())
		return
	}
	p.view.ShowPublishSuccess()
}

Kapan Memilih Pola Mana #

Gunakan MVC jika:
  ✓ Web server-side rendering (menggunakan HTML template)
  ✓ API backend yang sederhana
  ✓ Tim familier dengan MVC dan tidak butuh testability tinggi untuk UI

Gunakan MVP jika:
  ✓ Aplikasi mobile (Android, iOS) dengan interaksi UI yang signifikan
  ✓ Desktop application
  ✓ Butuh unit test Presenter tanpa menjalankan UI
  ✓ View perlu bisa diganti (misal: web view vs native view)

Gunakan MVVM jika:
  ✓ Framework mendukung data binding (Flutter, React, Jetpack Compose, SwiftUI)
  ✓ UI yang sangat reaktif dengan banyak state yang saling berkaitan
  ✓ Tim familiar dengan reactive programming

Gunakan MVVP jika:
  ✓ Butuh state UI yang eksplisit dan terstruktur (pagination, search, filter)
  ✓ UI adalah wizard atau workflow multi-step
  ✓ Butuh kontrol lebih detail dibanding MVP tapi tidak mau data binding MVVM

Checklist Review MVP/MVVM/MVVP #

VIEW:
  □ View tidak membuat keputusan — hanya render dan forward event
  □ View tidak mengakses service atau repository secara langsung
  □ View hanya memanggil method Presenter/ViewModel, tidak mengolah data
  □ View bisa diganti dengan implementasi lain tanpa mengubah Presenter

PRESENTER / VIEWMODEL:
  □ Tidak ada import dari framework UI (Android, iOS, Flutter, HTML)
  □ Semua business logic di service/domain, bukan di Presenter
  □ View Model (data untuk View) berbeda dari domain entity
  □ Formatting data (tanggal, angka, teks) dilakukan di Presenter, tidak di View
  □ Presenter bisa ditest dengan mock View tanpa menjalankan UI

MODEL:
  □ Domain entity tidak tahu tentang Presenter atau View
  □ Business rules ada di model/service, bukan di Presenter
  □ Repository interface ada di layer model

VIEW CONTRACT (INTERFACE):
  □ Interface didefinisikan dari sudut pandang Presenter kebutuhan
  □ Method di interface menggunakan bahasa UI ("showLoading", "showError")
  □ Interface tidak bergantung pada framework UI tertentu

PENGUJIAN:
  □ Presenter/ViewModel ditest dengan mock View
  □ Test memverifikasi bahwa method View yang tepat dipanggil
  □ Test mencakup skenario loading, error, empty, dan success

Ringkasan #

  • MVP, MVVM, dan MVVP semuanya mengatasi masalah MVC yang sama — View yang terlalu aktif dan Controller/Presenter yang terlalu gemuk; ketiganya memaksa View menjadi pasif.
  • Kunci MVP adalah View Contract (interface) — Presenter hanya tahu tentang interface View, bukan implementasinya; ini memungkinkan Presenter ditest dengan mock tanpa menjalankan UI.
  • Presenter tidak boleh bergantung pada framework UI — tidak ada import Android, iOS, atau Flutter di Presenter; ketergantungan pada UI framework membuat Presenter sulit ditest.
  • View Model berbeda dari domain entity — buat ViewModel/View Model struct yang spesifik untuk tampilan (format tanggal, teks yang sudah diformat, permission flags); jangan ekspos domain entity langsung ke View.
  • Formatting adalah tanggung jawab Presenter"2 hari lalu" bukan time.Time, "Rp 150.000" bukan int64; View hanya render string yang sudah siap.
  • MVVM menambahkan reaktivitas — state berubah di ViewModel, View otomatis update; cocok untuk framework yang mendukung data binding.
  • MVVP lebih eksplisit tentang state UI — View Presenter menyimpan semua state tampilan (pagination, search, sort) secara terstruktur; lebih tepat untuk UI yang kompleks.
  • God Presenter adalah anti-pattern utama — jika Presenter punya lebih dari 3-4 dependency atau lebih dari 300 baris, ini tanda Presenter perlu dipecah.
  • Business logic di Presenter adalah pelanggaran — aturan bisnis (“artikel minimal 100 karakter”) ada di domain atau service, bukan di Presenter; Presenter hanya mengorkestrasikan presentasi.
  • Ketiganya adalah pola presentasi, bukan arsitektur penuh — MVP/MVVM/MVVP menjawab bagaimana UI diorganisir; mereka sering dikombinasikan dengan Clean Architecture atau Layered Architecture untuk lapisan yang lebih dalam.

← Sebelumnya: MVC Architecture
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact