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

Как представлял данные во ViewModel в своей практике

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

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

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

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

Структура представления данных в ViewModel

В своей практике я выработал целостный подход к представлению данных в ViewModel, который сочетает принципы MVVM, реактивного программирования и чистой архитектуры. Вот ключевые аспекты:

1. Реактивные потоки данных с StateFlow/SharedFlow

Основной паттерн — использование StateFlow для состояния UI и SharedFlow для событий (one-shot events). Это заменяет устаревший LiveData в большинстве случаев.

class UserViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    // Состояние загрузки/ошибки/данных
    private val _userState = MutableStateFlow<UserState>(UserState.Loading)
    val userState: StateFlow<UserState> = _userState.asStateFlow()

    // События для навигации или одноразовых действий
    private val _events = MutableSharedFlow<UserEvent>()
    val events: SharedFlow<UserEvent> = _events.asSharedFlow()

    sealed class UserState {
        object Loading : UserState()
        data class Success(val user: User) : UserState()
        data class Error(val message: String) : UserState()
    }

    sealed class UserEvent {
        data class NavigateToProfile(val userId: String) : UserEvent()
        object ShowLogoutDialog : UserEvent()
    }
}

2. Принцип единого источника истины (Single Source of Truth)

Всегда стремлюсь к тому, чтобы данные в ViewModel были единственным источником истины для View:

class ProductsViewModel : ViewModel() {
    // Исходные данные из репозитория
    private val _allProducts = MutableStateFlow<List<Product>>(emptyList())
    
    // Производные состояния вычисляются на основе исходных данных
    val filteredProducts: StateFlow<List<Product>> = _allProducts
        .map { products -> products.filter { it.isAvailable } }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
    
    val totalPrice: StateFlow<Double> = _allProducts
        .map { products -> products.sumOf { it.price } }
        .stateIn(viewModelScope, SharingStarted.Lazily, 0.0)
}

3. Разделение на UI State и Business State

Я разделяю:

  • Business State — "сырые" данные из репозитория
  • UI State — данные, подготовленные для отображения
data class ArticleUiState(
    // Данные для отображения
    val title: String,
    val formattedDate: String,
    val authorName: String,
    val isBookmarked: Boolean,
    
    // Состояние UI
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

class ArticleViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(ArticleUiState(
        title = "",
        formattedDate = "",
        authorName = "",
        isBookmarked = false
    ))
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()
    
    private fun mapToUiState(article: Article, user: User): ArticleUiState {
        return ArticleUiState(
            title = article.title,
            formattedDate = formatDate(article.publishedDate), // Форматирование для UI
            authorName = "${user.firstName} ${user.lastName}",
            isBookmarked = user.bookmarks.contains(article.id)
        )
    }
}

4. Immutability и безопасность потоков

Все публичные потоки делаю read-only, а модификация происходит только через методы ViewModel:

class CartViewModel : ViewModel() {
    private val _cartItems = MutableStateFlow<List<CartItem>>(emptyList())
    val cartItems: StateFlow<List<CartItem>> = _cartItems.asStateFlow()
    
    // Публичный API для модификации
    fun addItem(product: Product) {
        _cartItems.update { currentItems ->
            val existing = currentItems.find { it.productId == product.id }
            existing?.let {
                currentItems.map { item ->
                    if (item.productId == product.id) item.copy(quantity = item.quantity + 1)
                    else item
                }
            } ?: (currentItems + CartItem.fromProduct(product))
        }
    }
    
    fun removeItem(productId: String) {
        _cartItems.update { it.filterNot { item -> item.productId == productId } }
    }
}

5. Обработка ошибок и состояний загрузки

Использую sealed class для явного представления всех возможных состояний:

sealed class DataState<out T> {
    object Loading : DataState<Nothing>()
    data class Success<T>(val data: T) : DataState<T>()
    data class Error(val exception: Throwable, val retryAction: () -> Unit) : DataState<Nothing>()
    object Empty : DataState<Nothing>()
}

class SearchViewModel : ViewModel() {
    private val _searchState = MutableStateFlow<DataState<List<SearchResult>>>(DataState.Empty)
    val searchState: StateFlow<DataState<List<SearchResult>>> = _searchState.asStateFlow()
    
    suspend fun search(query: String) {
        _searchState.value = DataState.Loading
        try {
            val results = repository.search(query)
            _searchState.value = if (results.isEmpty()) {
                DataState.Empty
            } else {
                DataState.Success(results)
            }
        } catch (e: Exception) {
            _searchState.value = DataState.Error(e) { search(query) }
        }
    }
}

6. Оптимизация производительности

  • Использую stateIn с правильной стратегией (SharingStarted.WhileSubscribed)
  • Применяю distinctUntilChanged() для предотвращения лишних обновлений
  • Использую flatMapLatest для отмены устаревших запросов

7. Тестируемость

Данные в ViewModel представляю так, чтобы их легко было тестировать:

// ViewModel с явными зависимостями
class PaymentsViewModel(
    private val paymentsRepository: PaymentsRepository,
    private val dateFormatter: DateFormatter,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
    // Чистые функции для преобразования данных
    private fun formatPayments(payments: List<Payment>): List<PaymentUi> {
        return payments.map { payment ->
            PaymentUi(
                id = payment.id,
                amount = formatCurrency(payment.amount),
                date = dateFormatter.format(payment.date),
                status = payment.status.toUiStatus()
            )
        }
    }
}

Такой подход обеспечивает:

  • Предсказуемость поведения UI
  • Тестируемость бизнес-логики
  • Масштабируемость при росте приложения
  • Безопасность относительно жизненного цикла компонентов
  • Реактивность и отзывчивость интерфейса

Ключевой принцип: ViewModel должна содержать минимально необходимые данные для отображения текущего состояния экрана, преобразованные и готовые к потреблению View слоем.