Какие знаешь способы отмены запросов для Presenter у View в MVP?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы отмены запросов для 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 кода |
Рекомендации по реализации
- Единообразие: выберите один подход в проекте для согласованности.
- Очистка в detachView(): всегда отменяйте запросы при отсоединении View.
- Проверка View на null: после асинхронного ответа убедитесь, что View всё ещё attached.
- Отмена старых запросов: при повторных вызовах (например, обновление данных) отменяйте предыдущие запросы.
- Использование DI: внедряйте зависимости (как SchedulerProvider) для тестируемости.
Лучшей практикой сейчас считается использование Kotlin Coroutines с viewModelScope или собственной CoroutineScope, так как это современный, эффективный и официально поддерживаемый Google подход. Для проектов, уже использующих RxJava, CompositeDisposable остаётся стандартным решением. Ключевое — гарантировать, что ни один запрос не переживет свой Presenter и не попытается обновить уничтоженную View.