Как представлял данные во ViewModel в своей практике
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Структура представления данных в 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 слоем.