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"bukantime.Time,"Rp 150.000"bukanint64; 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