MVVM is an architectural pattern that separates the user interface development from the business logic and data model. It was introduced by Microsoft architects to simplify event-driven programming of user interfaces.
Core Components:
Model
Represents the data and business logic
Contains the application's data and rules
Independent of the user interface
Manages data operations, storage, and retrieval
Typically includes data models, network calls, and data manipulation logic
View
Represents the user interface
Displays data to the user
Handles user interactions
Observes changes in the ViewModel
Passive and doesn't contain complex logic
ViewModel
Acts as a bridge between Model and View
Transforms Model data for display
Handles UI-related logic
Exposes data and commands to the View
Contains presentation logic
Uses data binding to update the View
Key Characteristics:
Data Binding: Automatically synchronizes data between View and ViewModel
Reactive Programming: Often uses observables and reactive streams
Separation of Concerns: Clear separation of UI, logic, and data layers
Testability: Easy to unit test due to clear component responsibilities
How MVVM Works:
User interacts with the View
View sends the action to ViewModel
ViewModel processes the action
ViewModel interacts with the Model to fetch/update data
Model returns data to ViewModel
ViewModel transforms and prepares data
View is automatically updated through data binding
Advantages:
Improved separation of concerns
Enhanced testability
Easier maintenance
Supports complex user interfaces
Facilitates parallel development
Reduces boilerplate code
Supports reactive programming paradigms
Class Diagram:
Code:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
// Data Model
data class User(
val id: String,
val name: String,
val email: String
)
// Model Interface
interface UserModel {
fun fetchUsers(): List<User>
fun addUser(user: User)
fun deleteUser(userId: String)
fun observeDataChanges(): Flow<List<User>>
}
// View Interface
interface UserView {
fun render(users: List<User>)
fun showError(message: String)
fun handleUserInteraction()
}
// ViewModel Interface
interface UserViewModel {
val usersState: StateFlow<List<User>>
fun loadUsers()
fun addNewUser(name: String, email: String)
fun deleteUser(userId: String)
}
// Concrete Model Implementation
class InMemoryUserModel : UserModel {
private val users = mutableListOf(
User("1", "John Doe", "john@example.com"),
User("2", "Jane Smith", "jane@example.com")
)
override fun fetchUsers(): List<User> = users.toList()
override fun addUser(user: User) {
users.add(user)
}
override fun deleteUser(userId: String) {
users.removeIf { it.id == userId }
}
override fun observeDataChanges(): Flow<List<User>> {
// In a real-world scenario, this would be a more complex observable mechanism
return MutableStateFlow(users)
}
}
// Concrete ViewModel Implementation
class UserManagementViewModel(private val model: UserModel) : UserViewModel {
private val _usersState = MutableStateFlow<List<User>>(emptyList())
override val usersState: StateFlow<List<User>> = _usersState.asStateFlow()
override fun loadUsers() {
// In a real app, this would likely be an asynchronous operation
val users = model.fetchUsers()
_usersState.value = users
}
override fun addNewUser(name: String, email: String) {
val newUser = User(
id = (usersState.value.size + 1).toString(),
name = name,
email = email
)
model.addUser(newUser)
_usersState.update { currentUsers -> currentUsers + newUser }
}
override fun deleteUser(userId: String) {
model.deleteUser(userId)
_usersState.update { currentUsers ->
currentUsers.filter { it.id != userId }
}
}
}
// Concrete View Implementation (Console-based for demonstration)
class ConsoleUserView(private val viewModel: UserViewModel) : UserView {
override fun render(users: List<User>) {
println("\n--- Current Users ---")
users.forEach { user ->
println("ID: ${user.id}, Name: ${user.name}, Email: ${user.email}")
}
}
override fun showError(message: String) {
println("Error: $message")
}
override fun handleUserInteraction() {
while (true) {
println("\nChoose an action:")
println("1. View Users")
println("2. Add User")
println("3. Delete User")
println("4. Exit")
print("Enter your choice: ")
when (readLine()?.trim()) {
"1" -> {
// Render current users
render(viewModel.usersState.value)
}
"2" -> {
// Add user
print("Enter user name: ")
val name = readLine() ?: return
print("Enter user email: ")
val email = readLine() ?: return
viewModel.addNewUser(name, email)
}
"3" -> {
// Delete user
print("Enter user ID to delete: ")
val userId = readLine() ?: return
viewModel.deleteUser(userId)
}
"4" -> {
println("Exiting...")
return
}
else -> showError("Invalid choice")
}
}
}
}
// MVVM Application Runner
class MVVMUserManagementApp {
companion object {
@JvmStatic
fun main(args: Array<String>) {
// Create MVVM components
val model = InMemoryUserModel()
val viewModel = UserManagementViewModel(model)
val view = ConsoleUserView(viewModel)
// Initial load of users
viewModel.loadUsers()
// Start user interaction
view.handleUserInteraction()
}
}
}
// Bonus: Extension function to observe StateFlow (simulating reactive behavior)
fun <T> StateFlow<T>.observe(action: (T) -> Unit) {
runBlocking {
action(value)
}
}