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

Как в MVI реализовать загрузку списка

2.0 Middle🔥 123 комментариев
#Архитектура и паттерны#Многопоточность и асинхронность

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

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

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

Реализация загрузки списка в MVI на Android

В архитектуре MVI (Model-View-Intent) загрузка списка реализуется через строгое разделение на компоненты с односторонним потоком данных. Вот пошаговая реализация:

1. Определение состояний (State)

Первым шагом создаем sealed class для всех возможных состояний экрана со списком:

sealed class ListState {
    object Loading : ListState()
    data class Success(val items: List<Item>) : ListState()
    data class Error(val message: String) : ListState()
    object Empty : ListState()
}

2. Определение намерений (Intent)

Intent'ы представляют пользовательские действия и системные события:

sealed class ListIntent {
    object LoadItems : ListIntent()
    object RefreshItems : ListIntent()
    data class ItemClicked(val item: Item) : ListIntent()
}

3. Реализация ViewModel с редюсером

В ViewModel создаем StateFlow для состояния и обрабатываем интенты:

class ListViewModel(
    private val repository: ItemsRepository
) : ViewModel() {
    
    private val _state = MutableStateFlow<ListState>(ListState.Loading)
    val state: StateFlow<ListState> = _state.asStateFlow()
    
    private val _intent = Channel<ListIntent>()
    
    init {
        handleIntents()
        sendIntent(ListIntent.LoadItems)
    }
    
    fun sendIntent(intent: ListIntent) {
        viewModelScope.launch {
            _intent.send(intent)
        }
    }
    
    private fun handleIntents() {
        viewModelScope.launch {
            _intent.consumeAsFlow().collect { intent ->
                when (intent) {
                    is ListIntent.LoadItems -> loadItems()
                    is ListIntent.RefreshItems -> refreshItems()
                    else -> {/* обработка других интентов */}
                }
            }
        }
    }
    
    private suspend fun loadItems() {
        _state.value = ListState.Loading
        try {
            val items = repository.getItems()
            _state.value = if (items.isEmpty()) {
                ListState.Empty
            } else {
                ListState.Success(items)
            }
        } catch (e: Exception) {
            _state.value = ListState.Error(e.message ?: "Unknown error")
        }
    }
    
    private suspend fun refreshItems() {
        loadItems() // или отдельная логика обновления
    }
}

4. Реализация UI слоя

В Activity/Fragment наблюдаем за состоянием и отображаем соответствующий UI:

class ListActivity : AppCompatActivity() {
    
    private val viewModel: ListViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.state.collect { state ->
                    render(state)
                }
            }
        }
    }
    
    private fun render(state: ListState) {
        when (state) {
            is ListState.Loading -> {
                showLoading()
                hideList()
                hideError()
            }
            is ListState.Success -> {
                hideLoading()
                showList(state.items)
                hideError()
            }
            is ListState.Error -> {
                hideLoading()
                hideList()
                showError(state.message)
            }
            is ListState.Empty -> {
                hideLoading()
                showEmptyState()
                hideError()
            }
        }
    }
    
    // Обработка кликов
    private fun onItemClick(item: Item) {
        viewModel.sendIntent(ListIntent.ItemClicked(item))
    }
}

5. Оптимизация с дополнительными эффектами (SingleLiveEvent)

Для разовых событий (навигация, тосты) используем дополнительный канал:

class ListViewModel(...) : ViewModel() {
    
    // ... существующий код ...
    
    private val _effects = Channel<ListEffect>()
    val effects: Flow<ListEffect> = _effects.receiveAsFlow()
    
    sealed class ListEffect {
        data class ShowMessage(val text: String) : ListEffect()
        data class NavigateToDetails(val itemId: String) : ListEffect()
    }
    
    private fun handleItemClick(item: Item) {
        viewModelScope.launch {
            _effects.send(ListEffect.NavigateToDetails(item.id))
        }
    }
}

Ключевые преимущества такого подхода:

  • Предсказуемость состояния: Все возможные состояния явно декларированы
  • Тестируемость: ViewModel легко тестируется, так как содержит чистую бизнес-логику
  • Воспроизводимость: Любое состояние можно воссоздать, воспроизведя последовательность интентов
  • Декларативность: UI просто отражает текущее состояние, не содержит логики принятия решений

Важные моменты для production-реализации:

  1. Кэширование данных: Добавьте кэш в репозиторий для избежания лишних запросов
  2. Пагинация: Для больших списков реализуйте LoadMore интент
  3. Retry логика: Добавьте RetryLoad интент для обработки ошибок
  4. DiffUtil: Используйте ListAdapter для эффективных обновлений RecyclerView
  5. Сохраняемость состояния: Конфигурационные изменения не нарушат загрузку благодаря StateFlow

Эта реализация обеспечивает чистую одностороннюю архитектуру, где состояние экрана полностью определяется последовательностью интентов, что упрощает отладку и поддержку кода.

Как в MVI реализовать загрузку списка | PrepBro