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

Какие знаешь способы организовать Single Event через ViewModel?

3.0 Senior🔥 131 комментариев
#Архитектура и паттерны

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

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

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

Способы организации Single Event через ViewModel

В архитектуре MVVM с использованием ViewModel и LiveData/StateFlow существует распространённая проблема: события, которые должны быть обработаны только один раз (например, навигация, показ тостов, диалогов). Если использовать обычные LiveData, событие может быть обработано повторно после поворота экрана или при переподписке. Вот основные способы решения этой проблемы.

1. Event Wrapper (Обёртка для событий)

Наиболее популярный подход — оборачивать событие в класс, который отслеживает, было ли оно уже обработано. Это классическое решение, описанное в статье "SingleLiveEvent" от Google.

// Обёртка для события
open class Event<out T>(private val content: T) {
    private var hasBeenHandled = false

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
}

// Использование в ViewModel
class MyViewModel : ViewModel() {
    private val _navigateEvent = MutableLiveData<Event<String>>()
    val navigateEvent: LiveData<Event<String>> = _navigateEvent

    fun performNavigation() {
        _navigateEvent.value = Event("destination")
    }
}

// Обработка во Fragment/Activity
viewModel.navigateEvent.observe(viewLifecycleOwner) { event ->
    event.getContentIfNotHandled()?.let { destination ->
        // Навигация только один раз
        navigateTo(destination)
    }
}

2. SingleLiveEvent (Специализированный LiveData)

Кастомная реализация LiveData, которая отправляет события только первым подписчикам, появившимся после изменения значения.

class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) { value ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(value)
            }
        }
    }

    override fun setValue(value: T?) {
        pending.set(true)
        super.setValue(value)
    }
}

Недостаток: только один подписчик получит событие, что может быть проблематично при нескольких observer'ах.

3. Kotlin Flow с SharedFlow и replay=0

Использование Kotlin Flow предоставляет более гибкий механизм. SharedFlow с replay = 0 не хранит предыдущие события.

class MyViewModel : ViewModel() {
    private val _toastEvent = MutableSharedFlow<String>()
    val toastEvent: SharedFlow<String> = _toastEvent.asSharedFlow()

    suspend fun showToast(message: String) {
        _toastEvent.emit(message)
    }
}

// В Fragment с lifecycleScope
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.toastEvent.collect { message ->
            showToast(message) // Событие будет получено только один раз
        }
    }
}

Для UI-событий часто используют SharedFlow с replay = 0 и extraBufferCapacity = 1.

4. Channel API (для одноразовых событий)

Channel — это примитив Kotlin Coroutines для коммуникации между корутинами, идеально подходящий для одноразовых событий.

class MyViewModel : ViewModel() {
    private val _eventChannel = Channel<Event>(capacity = Channel.BUFFERED)
    val eventFlow = _eventChannel.receiveAsFlow()

    sealed class Event {
        data class ShowSnackbar(val message: String) : Event()
        object NavigateBack : Event()
    }

    fun triggerEvent() {
        viewModelScope.launch {
            _eventChannel.send(Event.ShowSnackbar("Hello!"))
        }
    }
}

Преимущество: чёткое разделение между состоянием (StateFlow) и событиями (Channel/SharedFlow).

5. Реактивные расширения (RxJava)

При использовании RxJava можно применять операторы .distinctUntilChanged() или специальные Subject'ы.

private val _navigationSubject = PublishSubject.create<String>()
val navigationEvents: Observable<String> = _navigationSubject.hide()

fun navigateTo(destination: String) {
    _navigationSubject.onNext(destination)
}

Критерии выбора подхода

  • Для новых проектов предпочтительнее Kotlin Flow с SharedFlow, так как это современная, интегрированная с корутинами система, поддерживаемая Google.
  • Event Wrapper — надёжное, проверенное решение, но добавляет шаблонный код.
  • SingleLiveEvent считается устаревшим подходом, не рекомендуется к использованию в новых проектах.
  • Channels хорошо подходят для сложных сценариев с несколькими типами событий.

Ключевой принцип: разделяйте состояние (которое должно сохраняться при повороте) и события (которые должны срабатывать однократно). Современная архитектура рекомендует использовать StateFlow для состояния и SharedFlow с replay=0 или Channels для событий. Это обеспечивает чёткое разделение ответственности и предотвращает повторную обработку событий при изменении конфигурации устройства.