← Назад к вопросам

Какие знаешь способы отмены запросов для Presenter у View в MVP?

1.7 Middle🔥 151 комментариев
#Архитектура и паттерны#Жизненный цикл и навигация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы отмены запросов для Presenter в MVP

В архитектуре MVP (Model-View-Presenter) управление жизненным циклом запросов критически важно для предотвращения утечек памяти и некорректного поведения UI. Presenter выступает посредником между View и Model, и ему часто делегируют отмену асинхронных операций при уничтожении View (например, при повороте экрана или закрытии фрагмента). Вот основные подходы:

1. Использование RxJava (Disposable)

В RxJava каждый запрос возвращает Disposable, который можно сохранить в CompositeDisposable и очистить при разрушении Presenter'a.

class UserPresenter(
    private val userRepository: UserRepository,
    private val schedulers: SchedulerProvider
) {
    private val compositeDisposable = CompositeDisposable()
    private var view: UserView? = null

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

    fun loadUser(userId: String) {
        val disposable = userRepository.getUser(userId)
            .subscribeOn(schedulers.io())
            .observeOn(schedulers.ui())
            .subscribe(
                { user -> view?.showUser(user) },
                { error -> view?.showError(error.message) }
            )
        compositeDisposable.add(disposable)
    }

    fun detachView() {
        view = null
        // Отменяем все текущие запросы, связанные с View
        compositeDisposable.clear()
    }
}

Ключевые моменты:

  • CompositeDisposable собирает все Disposable объекты.
  • При detachView() вызов clear() отменяет все подписки.
  • Можно использовать Disposable одиночные, но CompositeDisposable удобнее для управления множеством запросов.

2. Coroutines (Job и CoroutineScope)

В Kotlin Coroutines управление жизненным циклом осуществляется через Job и CoroutineScope.

class ProductPresenter(
    private val productRepository: ProductRepository
) : BasePresenter<ProductView>() {
    // Создаем собственную область видимости корутин для Presenter
    private val presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    fun loadProducts() {
        presenterScope.launch {
            try {
                view?.showLoading()
                val products = withContext(Dispatchers.IO) {
                    productRepository.fetchProducts()
                }
                view?.showProducts(products)
            } catch (e: Exception) {
                view?.showError(e.message)
            } finally {
                view?.hideLoading()
            }
        }
    }

    override fun destroy() {
        super.destroy()
        // Отменяем все корутины в scope
        presenterScope.cancel()
    }
}

// Базовый класс для общего управления
open class BasePresenter<T : Any> {
    protected var view: T? = null

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

    fun detachView() {
        view = null
    }

    open fun destroy() {
        // Хук для очистки ресурсов
    }
}

Преимущества:

  • Structured Concurrency: корутины автоматически отменяются при отмене родительской Job.
  • SupervisorJob позволяет независимо обрабатывать сбои в отдельных корутинах.
  • Можно использовать viewModelScope в Android, если Presenter интегрирован с ViewModel.

3. Retrofit Call и отмена через Call.cancel()

При прямом использовании Retrofit можно сохранять ссылки на объекты Call и отменять их явно.

interface UserApi {
    @GET("users/{id}")
    fun getUser(@Path("id") userId: String): Call<UserResponse>
}

class UserPresenter(private val api: UserApi) {
    private var currentCall: Call<UserResponse>? = null
    private var view: UserView? = null

    fun loadUser(userId: String) {
        currentCall?.cancel() // Отменяем предыдущий запрос
        val call = api.getUser(userId)
        currentCall = call
        
        call.enqueue(object : Callback<UserResponse> {
            override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) {
                if (response.isSuccessful) {
                    view?.showUser(response.body())
                } else {
                    view?.showError("Server error")
                }
            }

            override fun onFailure(call: Call<UserResponse>, t: Throwable) {
                if (!call.isCanceled) { // Игнорируем ошибки от отмены
                    view?.showError(t.message)
                }
            }
        })
    }

    fun detachView() {
        view = null
        currentCall?.cancel()
        currentCall = null
    }
}

Особенности:

  • Проверка call.isCanceled в onFailure позволяет игнорировать ошибки отмены.
  • Можно хранить несколько Call объектов в коллекции для массовой отмены.

4. Паттерн "Use Case" с управлением жизненным циклом

В более сложных архитектурах (Clean Architecture) каждый UseCase может возвращать объект отмены.

class GetUserUseCase(
    private val userRepository: UserRepository
) {
    // Возвращает Disposable для внешнего управления
    fun execute(
        userId: String,
        onSuccess: (User) -> Unit,
        onError: (Throwable) -> Unit
    ): Disposable {
        return userRepository.getUser(userId)
            .subscribe(onSuccess, onError)
    }
}

class UserPresenter(
    private val getUserUseCase: GetUserUseCase
) {
    private val disposables = CompositeDisposable()

    fun loadUser(userId: String) {
        val disposable = getUserUseCase.execute(
            userId,
            onSuccess = { view?.showUser(it) },
            onError = { view?.showError(it.message) }
        )
        disposables.add(disposable)
    }

    fun detachView() {
        disposables.clear()
    }
}

5. Интеграция с жизненным циклом Android (Architecture Components)

Если Presenter является частью ViewModel или использует Lifecycle, можно привязать отмену к событиям жизненного цикла.

class LiveDataPresenter(
    private val userRepository: UserRepository
) : ViewModel() {
    private val _userLiveData = MutableLiveData<User>()
    val userLiveData: LiveData<User> = _userLiveData
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            val user = userRepository.getUser(userId)
            _userLiveData.value = user
        }
    }
    // viewModelScope автоматически отменяет корутины при очистке ViewModel
}

Сравнение подходов

СпособПлюсыМинусы
RxJavaБогатые операторы, легко комбинироватьСложность обучения, overhead библиотеки
CoroutinesОфициальная поддержка Kotlin, читаемый кодТребует понимания Structured Concurrency
Retrofit CallПростота, минимальные зависимостиРучное управление, меньше возможностей
Use CaseЧистая архитектура, тестируемостьБольше boilerplate кода

Рекомендации по реализации

  1. Единообразие: выберите один подход в проекте для согласованности.
  2. Очистка в detachView(): всегда отменяйте запросы при отсоединении View.
  3. Проверка View на null: после асинхронного ответа убедитесь, что View всё ещё attached.
  4. Отмена старых запросов: при повторных вызовах (например, обновление данных) отменяйте предыдущие запросы.
  5. Использование DI: внедряйте зависимости (как SchedulerProvider) для тестируемости.

Лучшей практикой сейчас считается использование Kotlin Coroutines с viewModelScope или собственной CoroutineScope, так как это современный, эффективный и официально поддерживаемый Google подход. Для проектов, уже использующих RxJava, CompositeDisposable остаётся стандартным решением. Ключевое — гарантировать, что ни один запрос не переживет свой Presenter и не попытается обновить уничтоженную View.

Какие знаешь способы отмены запросов для Presenter у View в MVP? | PrepBro