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

Для чего нужна Model в MVVM?

1.2 Junior🔥 191 комментариев
#Android компоненты#Архитектура и паттерны

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

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

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

# 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 даёт.

Для чего нужна Model в MVVM? | PrepBro