MVVM Architecture Pattern

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:

  1. 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

  2. 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

  3. 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:

  1. User interacts with the View

  2. View sends the action to ViewModel

  3. ViewModel processes the action

  4. ViewModel interacts with the Model to fetch/update data

  5. Model returns data to ViewModel

  6. ViewModel transforms and prepares data

  7. 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)
    }
}

Last updated