37
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RetailAI AdventurersAdvent Calendar 2023

Day 16

MVVM architecture (Model-View-ViewModel)

Last updated at Posted at 2023-12-06

Overview

MVVM stands for Model-View-ViewModel, which is a software architectural pattern used in application development, particularly in user interface (UI) development. MVVM is often associated with frameworks like Xamarin, Angular, and Vue.js for web and mobile application development (Android, iOS, Flutter).

mvvm.png

This image illustrates the MVVM architecture applied to Android applications.

The MVVM pattern separates an application into three main components:

  1. Model: The Model represents the data and business logic of the application. It encapsulates the application's data and is responsible for tasks such as data retrieval, storage, and manipulation. The Model is independent of the user interface and does not have any direct knowledge of how the data is presented to the user.
  2. View: The View is responsible for presenting the user interface to the user. It represents the visual elements and controls that users interact with. In a well-structured MVVM application, the View is as dumb as possible, meaning it should contain minimal logic and be primarily concerned with displaying data and handling user input.
  3. ViewModel: The ViewModel acts as an intermediary between the Model and the View. It contains the presentation logic and transforms the data from the Model into a format that can be easily displayed by the View. The ViewModel also handles user interactions and forwards relevant data and commands to the Model. Importantly, the ViewModel does not have direct knowledge of the View; it communicates with the View through data binding or other mechanisms provided by the framework.

Key features and concepts associated with MVVM:

  • Data Binding: One of the central features of MVVM is data binding, which enables automatic synchronization of data between the ViewModel and the View. Changes in the ViewModel are reflected in the View, and user interactions in the View are automatically propagated to the ViewModel.
  • Commands: MVVM often involves the use of commands or actions that are triggered by user interactions in the View. These commands are bound to methods or functions in the ViewModel, allowing for the separation of UI-related logic from the View itself.
  • Testing: MVVM facilitates unit testing because the ViewModel can be tested independently of the View. This separation of concerns makes it easier to write tests for the application's logic.
  • Reusability: MVVM promotes the reusability of ViewModels since they are not tightly coupled to specific Views. A single ViewModel can be used with different Views, making it easier to create multiple UIs for the same data and logic.
  • Maintainability: By separating concerns and promoting a clear separation between the View and the ViewModel, MVVM can lead to more maintainable and easily extensible codebases.

Implementation (Example for Android / Kotlin app)

This example is a simple test application that fetches Status API and display on the screen a String with an error or success message.

Model

The model is responsible for fetching the data from the network layer (in our case REST HTTP APIs with Retrofit) or the local database. For this example, we are calling the Status API.

class TestRepository(
    // Retrofit configuration for our status REST API
    private val statusAPI: statusAPI
) {

    // Service to fetch the status
    suspend fun getStatusRetrofit(): StatusResponse {
        return try {
            val response = statusAPI.getStatus(
                contentType = "application/json"
            )
            Log.e(
                "getState() Success",
                response.data.message
            )
            // Simple response handler with Success and Error
            StatusResponse.Success(response)
        } catch (e: Exception) {
            Log.e("getState() Error", e.localizedMessage ?: "Something went wrong with getState()")
            // Simple response handler with Success and Error
            StatusResponse.Error(e.localizedMessage ?: "Something went wrong with getState()")
        }
    }
}

Retrofit API

interface StatusAPI {

    @GET("/status")
    suspend fun getStatus(
        @Header("Content-Type") contentType: String
    ): Status

}

ViewModel

The ViewModel will be responsible for fetching the data from the TestRepository (Model) and sending it to the View via LiveData (MutableLiveData). Every time the Model responds the call with a new value the LiveData is updated triggering a new screen state on the View.

class TestViewModel(
    private val testRepository: TestRepository,
    private val dispatcher: CoroutineDispatcher
) :
    ViewModel() {

    private val _success = MutableLiveData<String>()
    val success: LiveData<String> = _success

    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error


    fun getStatus() {
        viewModelScope.launch(dispatcher) {
            when (val response = testRepository.getStatusRetrofit()) {
                is StatusResponse.Success -> {
                    _success.postValue("Response: " + response.result.data.message)
                }

                is StatusResponse.Error -> {
                    _error.postValue("Response: " + response.errorMessage)
                }
            }
        }
    }
}

LiveData

LiveData follows the observer pattern. LiveData notifies Observer objects when underlying data changes. You can consolidate your code to update the UI in these Observer objects. That way, you don't need to update the UI every time the app data changes because the observer does it for you.

Observers are bound to Lifecycle objects and clean up after themselves when their associated lifecycle is destroyed.

UI components just observe relevant data and don’t stop or resume observation. LiveData automatically manages all of this since it’s aware of the relevant lifecycle status changes while observing.

If an activity or fragment is recreated due to a configuration change, like device rotation, it immediately receives the latest available data.

The lifecycle of a ViewModel

The lifecycle of a ViewModel is tied directly to its scope. A ViewModel remains in memory until the ViewModelStoreOwner to which it is scoped disappears. This may occur in the following contexts:

  • In the case of an activity, when it finishes.
  • In the case of a fragment, when it detaches.
  • In the case of a Navigation entry, when it's removed from the back stack.

This makes ViewModels a great solution for storing data that survives configuration changes.

Figure 1 illustrates the various lifecycle states of an activity as it undergoes a rotation and then is finished. The illustration also shows the lifetime of the ViewModel next to the associated activity lifecycle. This particular diagram illustrates the states of an activity. The same basic states apply to the lifecycle of a fragment.

As your data grows more complex, you might choose to have a separate class just to load the data. The purpose of ViewModel is to encapsulate the data for a UI controller to let the data survive configuration changes.
lifecycle.png

View

As described earlier the view should be as dumb as possible, so our example view only implements the ViewModel and is updated when the Status API response is complete.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // ViewModel creation is handled by Koin dependency injection on this example.
            val viewModel: TestViewModel = getViewModel()
            TestAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Data(viewModel = viewModel)
                    // Fetch status on view creation
                    viewModel.getStatus()
                }
            }
        }
    }
}

@Composable
fun Data(modifier: Modifier = Modifier, viewModel: TestViewModel) {
    val success = viewModel.success.observeAsState()
    val error = viewModel.error.observeAsState()

    val context = LocalContext.current

    Column(
        modifier
            .padding(30.dp)
            .fillMaxWidth()
            .wrapContentSize(Alignment.Center)
    ) {
        // Display a blue text for Success
        Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Text(
                text = success.value ?: "",
                modifier = modifier.padding(16.dp),
                textAlign = TextAlign.Center,
                style = typography.h4,
                color = Color.Blue
            )
        }
        // Display a red text for Error
        Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Text(
                text = error.value ?: "",
                modifier = modifier.padding(16.dp),
                textAlign = TextAlign.Center,
                style = typography.h4,
                color = Color.Red
            )
        }
    }
}

As you can see for this test application we instantiate the ViewModel on the view and set the observables to populate our view. The ViewModel will request the data from the repository and trigger the observable with new data when available, and the repository will call the API to fetch this data.

The repository has no awareness of the view and the view doesn’t know about the repository. The ViewModel can access the repository but doesn’t know about the View. With this flow a View can implement multiple ViewModels and a ViewModel can call multiple repositories.

MVVM is particularly popular in scenarios where you have complex UIs or where you need to support multiple platforms with shared business logic. It helps create applications that are easier to develop, test, and maintain while promoting a clean separation of concerns.

References

MVVM (Model View ViewModel) Architecture Pattern in Android
LiveData overview | Developers Android
ViewModel overview | Developers Android

37
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?