MVP — định nghĩa, mẫu Model-View-Presenter trong iOS và Android

Tác giả: IT Sectr Đã đăng: 2026-02-16 Thời gian đọc: 10 phút

MVP (Model-View-Presenter) — một mẫu kiến trúc trong đó Presenter đóng vai trò trung gian giữa Model và View thông qua giao diện ViewContract. Khác với MVC, nơi Controller quản lý trực tiếp View qua UIKit, Presenter không phụ thuộc vào framework — nó hoạt động thông qua trừu tượng hóa, giúp nó có thể kiểm thử mà không cần Android SDK hay UIKit. MVP được sử dụng rộng rãi trong phát triển Android trước Jetpack và vẫn phù hợp cho các dự án kế thừa. Xem thêm trong bài viết của Martin Fowler.

Chính

  • MVP — ba thành phần: Model (dữ liệu), View (giao diện), Presenter (logic và trạng thái)
  • ViewContract — giao diện qua đó Presenter giao tiếp với View, đảm bảo khả năng kiểm thử
  • Presenter — chứa toàn bộ logic nghiệp vụ, không phụ thuộc vào các lớp nền tảng Android/iOS
  • Passive View — View hoàn toàn thụ động, chỉ hiển thị dữ liệu theo lệnh của Presenter
  • MVP vs MVC — Presenter được kiểm thử bằng unit test, Controller trong MVC phụ thuộc vào UIKit/Android Framework

MVP là gì: bản chất của mẫu Model-View-Presenter

MVP (Model-View-Presenter) — một mẫu kiến trúc được Martin Fowler đề xuất vào đầu những năm 2000 như một sự tiến hóa của MVC để cải thiện khả năng kiểm thử giao diện người dùng. Model quản lý dữ liệu và logic nghiệp vụ, View chịu trách nhiệm hiển thị và xử lý đầu vào của người dùng, Presenter là thành phần trung tâm nhận sự kiện từ View, truy xuất dữ liệu từ Model và tạo trạng thái để hiển thị.

Sự khác biệt chính giữa MVP và MVC — Presenter không có tham chiếu trực tiếp đến View. Thay vào đó, Presenter tương tác với View thông qua giao diện ViewContract. View triển khai giao diện này và truyền chính nó cho Presenter. Điều này phá vỡ sự phụ thuộc vào UIKit (iOS) hoặc Android Framework — Presenter có thể được kiểm thử riêng rẽ với triển khai mock của ViewContract. Trong MVC, bộ điều khiển UIViewController cập nhật trực tiếp UILabel, trong MVP, Presenter gọi view.showName(name), và View quyết định cách hiển thị.

Thành phầnTrách nhiệmKhả năng kiểm thử
ModelDữ liệu, logic nghiệp vụ, gọi mạngUnit test (không phụ thuộc UI)
ViewHiển thị UI, truyền sự kiện cho PresenterTriển khai mock qua giao diện
PresenterLogic nghiệp vụ, quản lý trạng thái, điều hướngUnit test (qua mock ViewContract)

Nguyên tắc đơn trách nhiệm trong MVP được tuân thủ chặt chẽ hơn MVC: View chỉ chịu trách nhiệm hiển thị, Model chịu trách nhiệm dữ liệu, Presenter chịu trách nhiệm logic và điều phối. Trong các dự án thực tế, Presenter chiếm 40–60% mã màn hình, View — 20–30%, Model — 20–30%. Sự phân bố này cho phép kiểm thử logic nghiệp vụ chính mà không cần khởi chạy trình giả lập Android hoặc iOS.

MVP trong Android: Presenter, ViewContract và Activity

MVP trong Android sử dụng Activity hoặc Fragment làm View, triển khai ViewContract — một giao diện với các phương thức hiển thị dữ liệu. Presenter được tạo trong Activity, gắn View vào chính nó và quản lý việc tải dữ liệu. Khi màn hình xoay, Activity được tạo lại — Presenter có thể được giữ lại thông qua retain fragment hoặc bộ nhớ ngoài, giải quyết vấn đề mất trạng thái đặc trưng của MVC thuần túy.

kotlin
// ViewContract — giao diện để Presenter giao tiếp với View
interface UserView {
    fun showLoading()
    fun hideLoading()
    fun showUser(user: User)
    fun showError(message: String)
}

// Presenter — lớp logic có thể kiểm thử
class UserPresenter(
    private val repository: UserRepository
) {
    private var view: UserView? = null

    fun attachView(view: UserView) {
        this.view = view
    }

    fun detachView() {
        view = null
    }

    fun loadUser(userId: Int) {
        view?.showLoading()
        repository.getUser(userId) { result ->
            view?.hideLoading()
            result.onSuccess { user ->
                view?.showUser(user)
            }.onFailure { e ->
                view?.showError(e.message ?: "Unknown error")
            }
        }
    }
}

// View (Activity) triển khai giao diện
class UserActivity : AppCompatActivity(), UserView {
    private val presenter = UserPresenter(UserRepository())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        presenter.attachView(this)
        presenter.loadUser(42)
    }

    override fun onDestroy() {
        presenter.detachView()
        super.onDestroy()
    }

    override fun showUser(user: User) { /* cập nhật UI */ }
    override fun showLoading() { /* hiển thị ProgressBar */ }
    override fun hideLoading() { /* ẩn ProgressBar */ }
    override fun showError(message: String) { /* hiển thị Snackbar */ }
}

Quản lý vòng đời — một vấn đề chính của MVP trên Android. Activity bị hủy khi xoay màn hình, và presenter.attachView() được gọi lại trong onCreate(). Nếu việc tải dữ liệu không đồng bộ (RxJava, coroutines), khi hoàn thành View có thể đã bị tách. Giải pháp — hủy đăng ký trong detachView() hoặc sử dụng Loader từ Support Library (cho dự án không có Jetpack). Tại IT Sectr chúng tôi đã sử dụng kết hợp MVP + RxJava trong nhiều năm ở các dự án thương mại — mẫu này ổn định nhưng đòi hỏi kỷ luật trong quản lý đăng ký.

Retain fragment — cơ chế giữ Presenter khi xoay màn hình. Fragment không có UI (setRetainInstance(true)) sống lâu hơn Activity và giữ tham chiếu đến Presenter. Khi Activity được tạo lại, fragment truyền cùng Presenter đó cho Activity mới. Retain fragment đã không được dùng từ AndroidX, nhưng phiên bản pre-Jetpack của chúng (Fragment.setRetainInstance) vẫn hoạt động trong các dự án kế thừa. Trong phát triển hiện đại, Google khuyến nghị ViewModel thay vì retain fragment.

MVP trong iOS: Presenter và View Protocol

MVP trong iOS được xây dựng thông qua giao thức View. UIViewController triển khai giao thức, Presenter không import UIKit và hoàn toàn có thể kiểm thử. Khác với Apple MVC, nơi UIViewController tự chứa logic và kết nối IBOutlet trực tiếp, Presenter quản lý trạng thái và ra lệnh cho View thông qua các phương thức giao thức. View không đưa ra quyết định — nó thực thi các lệnh của Presenter: showUser, showLoading, navigateToProfile.

swift
import Foundation

// View Protocol — trừu tượng hóa cho Presenter
protocol UserViewProtocol: AnyObject {
    func showLoading()
    func hideLoading()
    func display(user: User)
    func displayError(message: String)
}

// Presenter — logic thuần túy, không UIKit
final class UserPresenter {
    private weak var view: UserViewProtocol?
    private let service: UserServiceProtocol

    init(service: UserServiceProtocol) {
        self.service = service
    }

    func attach(view: UserViewProtocol) {
        self.view = view
    }

    func detach() {
        view = nil
    }

    func loadUser(id: Int) {
        view?.showLoading()
        service.fetchUser(id: id) { [weak self] result in
            guard let self else { return }
            self.view?.hideLoading()
            switch result {
            case .success(let user):
                self.view?.display(user: user)
            case .failure(let error):
                self.view?.displayError(message: error.localizedDescription)
            }
        }
    }
}

// View (UIViewController) triển khai giao thức
final class UserViewController: UIViewController, UserViewProtocol {
    private let presenter = UserPresenter(service: UserService())

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.attach(view: self)
        presenter.loadUser(id: 42)
    }

    func display(user: User) {
        nameLabel.text = user.name
    }
    // ... các phương thức còn lại của giao thức
}

Weak reference vào View — điều kiện bắt buộc trong MVP iOS. UIViewController có thể bị hủy (pop khỏi navigation stack), và closure của nó trong Presenter sẽ tạo ra retain cycle. Tham chiếu yếu (weak var) đảm bảo View được giải phóng khi rời khỏi màn hình, bất kể các hoạt động không đồng bộ trong Presenter. Trong Android, vấn đề tương tự được giải quyết qua detachView() — gọi nó trong onDestroy() sẽ xóa tham chiếu đến View.

Passive View vs Supervising Controller — hai biến thể của MVP bởi Martin Fowler. Passive View: View không chứa logic, Presenter quản lý hoàn toàn trạng thái. Supervising Controller: View tự thực hiện ràng buộc dữ liệu đơn giản (ví dụ qua data binding), Presenter chỉ can thiệp trong các tình huống phức tạp. Trong phát triển di động, Passive View được sử dụng nhiều hơn — nó mang lại khả năng kiểm thử tối đa và tính dự đoán của trạng thái màn hình.

Khác biệt giữa MVP và MVC cùng lợi ích kiểm thử

Sự khác biệt chính giữa MVP và MVC là cách giao tiếp với View. Trong MVC, Controller có tham chiếu trực tiếp đến View (UIViewController.IBOutlets, Activity.findViewById). Trong MVP, Presenter tương tác với View thông qua giao diện ViewContract. Sự khác biệt này thay đổi căn bản khả năng kiểm thử: một đối tượng mock triển khai ViewContract cho phép kiểm thử logic của Presenter mà không cần khởi chạy ứng dụng, trình giả lập hoặc framework UI.

Tiêu chíMVCMVP
Kết nối với ViewTrực tiếp (Controller → View)Qua giao diện (Presenter → ViewContract)
Kiểm thử logicCần UIKit/Android FrameworkUnit test không phụ thuộc nền tảng
Vòng đờiController sống cùng màn hìnhPresenter có thể sống lâu hơn (retain)
Độ phức tạpTối thiểu+1 giao diện mỗi màn hình
Massive ControllerVấn đề điển hìnhLogic trong Presenter, View mỏng

Ví dụ unit test cho Presenter bằng Kotlin: tạo mock UserView, truyền vào Presenter, gọi loadUser, kiểm tra showUser được gọi với dữ liệu chính xác. Bài kiểm thử chạy trong mili giây, không cần trình giả lập. Trên iOS tương tự — OCMock hoặc stub giao thức xác minh các lời gọi phương thức UserViewProtocol. Trong các dự án IT Sectr với MVP, độ phủ unit test logic nghiệp vụ đạt 85–90%, cao gấp 2–3 lần so với các dự án MVC tương tự.

Khi nào MVP được ưu tiên hơn MVC — trong các dự án có yêu cầu ổn định khắt khe: ứng dụng ngân hàng, hệ thống y tế, thiết bị thanh toán. Trong các lĩnh vực này, chi phí lỗi cao và unit test rất quan trọng. Trong các dự án hậu MVP (khi sản phẩm đã có mặt trên thị trường nhưng mã nguồn kế thừa), MVP cho phép trích xuất dần logic từ Massive View Controller sang lớp có thể kiểm thử mà không cần viết lại toàn bộ kiến trúc.

Hạn chế của MVP và chuyển sang MVVM

Nhược điểm chính của MVP — sự gia tăng số lượng giao diện và quản lý đăng ký thủ công. Mỗi màn hình cần ít nhất một ViewContract + Presenter, cho 50 màn hình — 50 giao diện và 50 lớp Presenter. Trong MVVM, ViewModel thay thế Presenter và sử dụng cơ chế phản ứng (LiveData, StateFlow, ObservableObject), loại bỏ nhu cầu attach/detach thủ công và các giao diện ViewContract.

RxJava và MVP — sự kết hợp phổ biến trong Android 2015–2019. Presenter đăng ký Observable từ Repository, hiển thị kết quả qua ViewContract. Vấn đề: disposable phải được hủy rõ ràng trong detachView(), nếu không rò rỉ đăng ký sẽ gây ra sự cố khi cập nhật View đã tách. Các thư viện RxLifecycle và AutoDispose đã tự động hóa một phần việc hủy đăng ký nhưng thêm phụ thuộc. Tại IT Sectr chúng tôi đã chuyển từ MVP+RxJava sang MVVM+Flow vào năm 2020 — mã ngắn hơn 25–30% nhờ loại bỏ ViewContract.

Di chuyển từ MVP sang MVVM — một quá trình dần dần. 1) Thay thế ViewContract bằng LiveData/StateFlow trong Presenter. 2) Xóa các phương thức attach/detach — đăng ký thực hiện qua observe(). 3) Đổi tên Presenter thành ViewModel. 4) Tích hợp DI (Hilt/Koin) cho ViewModelFactory. Di chuyển một màn hình mất 2–4 giờ, toàn bộ mã nguồn — 2–4 tuần cho dự án 50–100 màn hình. Sau khi di chuyển, các giao diện ViewContract bị xóa, mã giảm, bài kiểm thử vẫn giữ nguyên.

MVP trong phát triển hiện đại — mẫu này còn sống nhưng nhường chỗ cho MVVM và MVI. Google chính thức khuyến nghị MVVM với Jetpack cho các dự án mới. Apple — MVVM với SwiftUI. Tuy nhiên, kiến thức về MVP là bắt buộc để làm việc với mã kế thừa: hàng trăm ứng dụng Android trên Google Play vẫn chạy trên MVP, bao gồm ứng dụng của các ngân hàng lớn, nhà bán lẻ và công ty vận tải. Hiểu MVP là nền tảng để làm chủ MVI và Clean Architecture, vì Presenter là tiền thân trực tiếp của Use Case theo thuật ngữ của Robert Martin.

Câu hỏi thường gặp

MVP khác MVC như thế nào?

Trong MVP, Presenter tương tác với View qua giao diện ViewContract, không trực tiếp. Trong MVC, Controller có tham chiếu trực tiếp đến View qua IBOutlet/findViewById. MVP cho phép kiểm thử Presenter bằng unit test mà không cần iOS Simulator hay Android Emulator, vì Presenter không phụ thuộc vào UIKit hay Android Framework. MVC yêu cầu khởi chạy ứng dụng để kiểm thử controller.

Khi nào nên dùng MVP thay vì MVVM?

MVP phù hợp trong các dự án kế thừa đã xây dựng trên mẫu này và trong các ứng dụng không hỗ trợ cơ chế phản ứng (LiveData, StateFlow, Combine). Cho các dự án mới, Google khuyến nghị MVVM với Jetpack (Android) và Apple khuyến nghị MVVM với SwiftUI (iOS). MVP vẫn là lựa chọn tốt nhất cho các dự án trên UIKit thuần túy không có Combine khi cần unit test logic nghiệp vụ.

Làm thế nào để giải quyết vấn đề mất Presenter khi xoay màn hình?

Trên Android — sử dụng retain fragment (setRetainInstance(true)) hoặc ViewModel từ Jetpack. Retain fragment lưu trữ Presenter khi xoay và truyền nó cho Activity mới. ViewModel của Google là giải pháp hiện đại tự động giữ trạng thái khi xoay mà không cần retain fragment. Trên iOS — Presenter được tạo lại mỗi viewDidLoad nhưng được lưu cache trong dịch vụ điều phối riêng.

Cần bao nhiêu lớp cho một màn hình trong MVP?

Tối thiểu 4: giao diện ViewContract, triển khai ViewContract (Activity/Fragment), Presenter, Model (Repository). Nếu sử dụng Dagger/Hilt, một mô-đun DI được thêm vào. Cho 50 màn hình — hơn 200 lớp. MVVM giảm 1 tệp mỗi màn hình (ViewContract không cần thiết), MVI thêm lớp State và Intent. Số lượng lớp là lý do chính phản đối MVP trong các dự án lớn.

Sự khác biệt giữa Passive View và Supervising Controller trong MVP là gì?

Passive View — View không chứa logic, Presenter quản lý hoàn toàn trạng thái và dữ liệu. Supervising Controller — View tự thực hiện ràng buộc đơn giản (data binding), Presenter chỉ can thiệp trong các tình huống phức tạp. Trong phát triển di động, Passive View chiếm ưu thế — nó mang lại khả năng kiểm thử và dự đoán tối đa. Supervising Controller được sử dụng trong các framework web (ASP.NET Web Forms, GWT).

Tổng kết

  • MVP (Model-View-Presenter) — sự tiến hóa của MVC với lớp Presenter có thể kiểm thử qua giao diện ViewContract
  • ViewContract — giao diện trừu tượng hóa View khỏi Presenter, cho phép kiểm thử mock
  • Passive View — biến thể MVP chiếm ưu thế trong phát triển di động với View thụ động
  • Presenter — chứa logic nghiệp vụ, không phụ thuộc UIKit hay Android Framework
  • MVP vs MVC — MVP giải quyết vấn đề kiểm thử nhưng thêm 1 giao diện mỗi màn hình
  • Retain fragment Android — giữ Presenter khi xoay màn hình trước Jetpack ViewModel
  • Chuyển sang MVVM — thay ViewContract bằng LiveData/StateFlow giảm mã 25–30%

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm