Архитектурные паттерны в Kotlin: MVC, MVP, MVVM и MVI

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как не утонуть в хаосе: архитектурные паттерны в Kotlin

Вы когда-нибудь открывали свой проект через месяц после написания и не могли понять, где находится логика авторизации? Или пытались добавить новую кнопку, а она ломала работу списка? Знакомо? Добро пожаловать в мир приложений без архитектуры.

Архитектурные паттерны в Kotlin — это не дань моде и не абстрактные теории. Это спасение от «лапшичного кода», когда всё перемешано: бизнес-логика, UI, работа с сетью и базой данных. В этой статье мы разберём четыре основных паттерна: MVC, MVP, MVVM и MVI. Вы увидите их в деле на реальных примерах Kotlin, поймёте ключевые отличия и сможете выбрать правильный инструмент для своей задачи. Поехали!

Что такое архитектурный паттерн и зачем он нужен?

Архитектурный паттерн — это скелет вашего приложения. Он диктует, как классы общаются друг с другом, где хранить данные и кто отвечает за отображение. Без него рано или поздно вы получите God Object — класс, который делает всё. Или Callback Hell — когда одни колбэки порождают другие, и код становится нечитаемым.

Kotlin, с его лаконичным синтаксисом и нулевой безопасностью, идеально подходит для чистой архитектуры. Но язык — это только половина дела. Вторая половина — правильный выбор паттерна.

MVC (Model-View-Controller) — классика жанра

Самый старый и простой паттерн. Его суть:

  • Model — данные и бизнес-логика.
  • View — отображение (Activity, Fragment, Compose).
  • Controller — связующее звено, обрабатывает ввод пользователя.

В Kotlin (особенно для Android) MVC часто превращается в «Толстый Controller». Activity становится и View, и Controller одновременно. Пример:

// Плохо: Activity — это и View, и Controller
class LoginActivity : AppCompatActivity() {
    private lateinit var emailEditText: EditText
    private lateinit var passwordEditText: EditText

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // Controller логика прямо здесь
        findViewById<Button>(R.id.loginButton).setOnClickListener {
            val email = emailEditText.text.toString()
            val password = passwordEditText.text.toString()
            // Валидация и вызов API — всё в одном месте
            if (email.isNotEmpty() && password.isNotEmpty()) {
                loginUser(email, password)
            }
        }
    }

    private fun loginUser(email: String, password: String) {
        // Работа с сетью — прямо в Activity
        CoroutineScope(Dispatchers.IO).launch {
            val result = api.login(email, password)
            withContext(Dispatchers.Main) {
                // Обновление UI — тоже тут
                Toast.makeText(this@LoginActivity, result.message, Toast.LENGTH_SHORT).show()
            }
        }
    }
}

Плюсы: простота, низкий порог входа.

Минусы: View и Controller жёстко связаны. Тестировать логику без эмулятора Android невозможно. Для больших проектов — ад.

MVP (Model-View-Presenter) — разделение властей

MVP решает главную проблему MVC: View отделяется от логики через интерфейсы. Presenter — это «мозг», который ничего не знает об Android. View — пассивная прослойка.

Пример на Kotlin:

// Интерфейсы для связи
interface LoginView {
    fun showLoading()
    fun hideLoading()
    fun showError(message: String)
    fun navigateToHome()
}

class LoginPresenter(private val view: LoginView, private val authRepository: AuthRepository) {

    fun login(email: String, password: String) {
        view.showLoading()
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val user = authRepository.login(email, password)
                withContext(Dispatchers.Main) {
                    view.hideLoading()
                    if (user != null) {
                        view.navigateToHome()
                    } else {
                        view.showError("Неверный email или пароль")
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    view.hideLoading()
                    view.showError(e.message ?: "Ошибка сети")
                }
            }
        }
    }
}

// Activity реализует интерфейс View
class LoginActivity : AppCompatActivity(), LoginView {
    private lateinit var presenter: LoginPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        presenter = LoginPresenter(this, AuthRepository())
        findViewById<Button>(R.id.loginButton).setOnClickListener {
            presenter.login("test@test.com", "password")
        }
    }

    override fun showLoading() { /* показать прогресс */ }
    override fun hideLoading() { /* скрыть */ }
    override fun showError(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() }
    override fun navigateToHome() { /* переход */ }
}

Плюсы: чистый Presenter легко тестируется (нет зависимости от Android). Чёткое разделение.

Минусы: много бойлерплейта (интерфейсы, методы). При утечке контекста (поворот экрана) Presenter может ссылаться на мёртвую View.

Архитектурные паттерны в Kotlin для Android часто выбирают MVP, если хотят строгий контроль. Но есть нюанс — жизненный цикл.

MVVM (Model-View-ViewModel) — реактивность и Jetpack

Современный стандарт для Android-приложений. MVVM использует LiveData или StateFlow для реактивной связи. ViewModel переживает повороты экрана, а View подписывается на изменения.

Пример с StateFlow:

// ViewModel — чистая логика, без ссылок на View
class LoginViewModel(private val authRepository: AuthRepository) : ViewModel() {

    private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()

    fun login(email: String, password: String) {
        _uiState.value = LoginUiState.Loading
        viewModelScope.launch {
            try {
                val user = authRepository.login(email, password)
                _uiState.value = if (user != null) {
                    LoginUiState.Success
                } else {
                    LoginUiState.Error("Неверные данные")
                }
            } catch (e: Exception) {
                _uiState.value = LoginUiState.Error(e.message ?: "Ошибка")
            }
        }
    }
}

// Состояние экрана — sealed class
sealed class LoginUiState {
    object Idle : LoginUiState()
    object Loading : LoginUiState()
    object Success : LoginUiState()
    data class Error(val message: String) : LoginUiState()
}

// Activity подписывается на StateFlow
class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        findViewById<Button>(R.id.loginButton).setOnClickListener {
            viewModel.login("test@test.com", "password")
        }

        // Реактивное обновление UI
        lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is LoginUiState.Loading -> showLoading()
                    is LoginUiState.Success -> navigateToHome()
                    is LoginUiState.Error -> showError(state.message)
                    else -> {}
                }
            }
        }
    }
}

Блоги

Рекомендаций по книгам