Для чего нужна Model в MVVM?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Model в MVVM: Назначение и роль
Что такое Model в MVVM
Model в MVVM — это слой, который содержит бизнес-логику и управление данными приложения. Model отвечает за работу с данными (их получение, обновление, сохранение) и реализацию бизнес-правил.
Это НЕ пользовательский интерфейс, а бизнес-логика, которая существует независимо от того, как её показывать.
Роли Model в MVVM
1. Управление состоянием данных
Model держит и управляет состоянием данных приложения:
// Model — это НЕ UI, это ДАННЫЕ
data class UserData {
val id: Int
val name: String
val email: String
val isActive: Boolean
val balance: Double
}
// Model может быть как простой объект данных
data class ShoppingCart(
val items: List<CartItem>,
val totalPrice: Double
)
// Так и класс с логикой
class ShoppingCartModel {
private val items = mutableListOf<CartItem>()
fun addItem(item: CartItem) {
items.add(item)
// Пересчитать скидки, налоги и т.д.
}
fun calculateTotal(): Double {
return items.sumOf { it.price }
}
fun applyPromoCode(code: String): Boolean {
// Бизнес-логика применения прому-кода
return true
}
}
2. Инкапсуляция бизнес-правил
Model инкапсулирует правила, как работают данные:
// Model с бизнес-логикой
class UserModel {
private var _balance: Double = 0.0
val balance: Double get() = _balance
fun withdraw(amount: Double): Boolean {
// Правило: нельзя снять больше, чем на счёте
if (amount > _balance) return false
// Правило: минимальная сумма 10
if (amount < 10) return false
_balance -= amount
return true
}
fun deposit(amount: Double) {
// Правило: депозит максимум 100,000 за раз
val actualAmount = minOf(amount, 100_000.0)
_balance += actualAmount
}
}
Видение: ViewModel и View не должны знать об этих правилах. Они просто вызывают методы Model.
3. Источник истины (Single Source of Truth)
Model — это единственный источник правильных данных:
// ViewModel НЕ хранит копию данных
class UserViewModel {
// ✗ ПЛОХО — дублирование данных
private val _userNameInViewModel = MutableStateFlow("")
private val _userEmailInViewModel = MutableStateFlow("")
// ✓ ХОРОШО — данные в Model, ViewModel только передаёт
fun loadUserData(): Flow<UserData> = userModel.observeUser()
}
// Model — истина
class UserModel {
private val _user = MutableStateFlow<UserData?>(null)
val user: StateFlow<UserData?> = _user.asStateFlow()
suspend fun loadUser(id: Int) {
_user.value = userRepository.getUser(id)
}
}
4. Независимость от UI
Model НЕ должна знать про Android, View, ViewModel.
// ✓ ХОРОШО — Model чистая, без зависимостей на UI
class CalculatorModel {
fun add(a: Double, b: Double): Double = a + b
fun subtract(a: Double, b: Double): Double = a - b
fun multiply(a: Double, b: Double): Double = a * b
}
// ✗ ПЛОХО — Model зависит от Android
class CalculatorModel {
fun add(a: Double, b: Double): Double {
Toast.makeText(context, "Adding numbers").show() // NO!
return a + b
}
}
Структура MVVM
┌─────────────────────────────────────────┐
│ View (UI) │
│ (Compose/XML, Activity, Fragment) │
│ Отображает данные, реагирует на клики │
└──────────────────┬──────────────────────┘
│
├─ collectAsState()
├─ observeUiState()
│
┌──────────────────▼──────────────────────┐
│ ViewModel │
│ • Управляет UI состоянием │
│ • Преобразует данные Model для View │
│ • НЕ содержит бизнес-логику │
└──────────────────┬──────────────────────┘
│
├─ вызывает
│
┌──────────────────▼──────────────────────┐
│ Model / Repository │
│ • Бизнес-логика │
│ • Управление данными │
│ • Работа с API, БД │
│ • Use Cases │
└─────────────────────────────────────────┘
Практический пример: Приложение для списка задач
Model Layer
// Use Case (часть Model)
class GetTasksUseCase(private val repository: TaskRepository) {
suspend operator fun invoke(): Result<List<Task>> = try {
Result.Success(repository.getTasks())
} catch (e: Exception) {
Result.Error(e)
}
}
class CompleteTaskUseCase(private val repository: TaskRepository) {
suspend operator fun invoke(taskId: Int): Result<Unit> = try {
val task = repository.getTask(taskId)
repository.updateTask(task.copy(isCompleted = true))
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
// Repository (часть Model)
interface TaskRepository {
suspend fun getTasks(): List<Task>
suspend fun getTask(id: Int): Task
suspend fun updateTask(task: Task)
suspend fun deleteTask(id: Int)
}
class TaskRepositoryImpl(
private val api: TaskApi,
private val db: TaskDao
) : TaskRepository {
override suspend fun getTasks(): List<Task> {
return try {
val remoteTasks = api.fetchTasks()
db.insertAll(remoteTasks.map { it.toEntity() })
remoteTasks.map { it.toDomain() }
} catch (e: Exception) {
db.getAllTasks().map { it.toDomain() }
}
}
override suspend fun updateTask(task: Task) {
api.updateTask(task.toDto())
db.insert(task.toEntity())
}
}
// Entity (часть Model)
data class Task(
val id: Int,
val title: String,
val description: String,
val isCompleted: Boolean = false,
val priority: Int = 0
) {
fun isOverdue(): Boolean = !isCompleted && priority > 5
}
ViewModel Layer
data class TaskListUiState(
val isLoading: Boolean = false,
val tasks: List<Task> = emptyList(),
val error: String? = null
)
class TaskListViewModel(
private val getTasksUseCase: GetTasksUseCase,
private val completeTaskUseCase: CompleteTaskUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(TaskListUiState())
val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()
init {
loadTasks()
}
private fun loadTasks() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
val result = getTasksUseCase() // Вызов бизнес-логики (Model)
_uiState.value = when (result) {
is Result.Success -> _uiState.value.copy(
isLoading = false,
tasks = result.data // Данные от Model
)
is Result.Error -> _uiState.value.copy(
isLoading = false,
error = result.error.message
)
}
}
}
fun completeTask(taskId: Int) {
viewModelScope.launch {
val result = completeTaskUseCase(taskId) // Вызов бизнес-логики
if (result is Result.Success) {
loadTasks() // Перезагрузить список
}
}
}
}
View Layer
@Composable
fun TaskListScreen(
viewModel: TaskListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
when {
uiState.isLoading -> CircularProgressIndicator()
uiState.error != null -> ErrorMessage(uiState.error)
else -> LazyColumn {
items(uiState.tasks) { task ->
TaskRow(
task = task,
onCompleteClick = { viewModel.completeTask(task.id) }
)
}
}
}
}
@Composable
fun TaskRow(
task: Task,
onCompleteClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Checkbox(
checked = task.isCompleted,
onCheckedChange = { onCompleteClick() }
)
Column(modifier = Modifier.weight(1f)) {
Text(task.title, fontWeight = FontWeight.Bold)
Text(task.description, style = MaterialTheme.typography.bodySmall)
}
}
}
Преимущества Model в MVVM
1. Тестируемость
// Тестируем Model без UI, без Context, без всего
class TaskRepositoryTest {
@Test
fun testGetTasksReturnsData() = runTest {
val mockApi = mockk<TaskApi>()
coEvery { mockApi.fetchTasks() } returns listOf(
TaskDto(1, "Task 1", "Desc 1")
)
val repository = TaskRepositoryImpl(mockApi, mockk())
val result = repository.getTasks()
assert(result.size == 1)
assert(result[0].title == "Task 1")
}
}
2. Переиспользование
Model можно использовать в разных View:
// View для телефона
@Composable
fun MobileTaskScreen(viewModel: TaskListViewModel) { ... }
// View для планшета
@Composable
fun TabletTaskScreen(viewModel: TaskListViewModel) { ... }
// View для ТВ
@Composable
fun TVTaskScreen(viewModel: TaskListViewModel) { ... }
// Все используют ОДИН и ТОТ ЖЕ ViewModel и Model
3. Масштабируемость
Велики объёмы сложности в Model, но View и ViewModel остаются простыми:
// Model может содержать сложную логику
class PaymentModel {
fun calculateTax(amount: Double): Double { ... } // Сложная логика
fun validateCreditCard(card: String): Boolean { ... } // Сложная логика
fun processPayment(payment: Payment): Result { ... } // Очень сложная логика
}
// Но ViewModel просто вызывает
class PaymentViewModel(val paymentModel: PaymentModel) {
fun pay(amount: Double) {
val result = paymentModel.processPayment(...)
}
}
// И View просто показывает
@Composable
fun PaymentScreen(viewModel: PaymentViewModel) {
Button(onClick = { viewModel.pay(100.0) }) {
Text("Pay")
}
}
4. Разделение ответственности
- Model — что делать
- ViewModel — как показать
- View — нарисовать
// Model знает про бизнес
class CurrencyModel {
fun convert(amount: Double, from: String, to: String): Double {
// Знает курсы валют, комиссии, правила банка
}
}
// ViewModel знает про UI логику
class CurrencyViewModel {
fun showConvertResult(amount: Double) {
// Знает, как показать результат красиво
}
}
// View знает только про отрисовку
@Composable
fun CurrencyScreen() {
// Только Compose код
}
Идеальный поток данных
User interacts (clicks button)
↓
View calls ViewModel method
↓
ViewModel calls Model (Use Case / Repository)
↓
Model performs business logic
↓
Model returns data
↓
ViewModel transforms data for UI
↓
ViewModel updates StateFlow
↓
View observes StateFlow and re-renders
Антипаттерны
✗ Model содержит UI логику
// ПЛОХО
class UserModel {
fun loadUser() {
showProgressDialog() // NO! UI логика в Model
val user = api.getUser()
showToast("User loaded") // NO!
}
}
✗ ViewModel содержит бизнес-логику
// ПЛОХО
class UserViewModel {
fun processPayment(amount: Double) {
val tax = amount * 0.1 // Бизнес-логика в ViewModel?
val total = amount + tax
api.pay(total) // API call в ViewModel?
}
}
// ХОРОШО
class UserViewModel(val paymentModel: PaymentModel) {
fun processPayment(amount: Double) {
val result = paymentModel.calculateTotal(amount) // Вызов Model
// ViewModel только управляет UI состоянием
}
}
✗ View содержит бизнес-логику
// ПЛОХО
@Composable
fun CheckoutScreen() {
Button(onClick = {
val tax = amount * 0.1 // Бизнес-логика в View?
val total = amount + tax
api.pay(total) // API call из View?
})
}
// ХОРОШО
@Composable
fun CheckoutScreen(viewModel: CheckoutViewModel) {
Button(onClick = { viewModel.processPayment() })
}
Вывод
Model в MVVM — это сердце приложения, которое:
- Содержит всю бизнес-логику
- Управляет данными
- Является истиной для приложения
- Независимо от UI фреймворка
- Легко тестируется
ViewModel просто оборачивает Model для UI нужд, а View просто показывает то, что ViewModel даёт.