Harnessing Android’s Full Potential: Embracing Model-View-ViewModel with Interactions (MVVI)

Kerry Bisset
Stackademic
Published in
7 min readNov 8, 2023

--

In the ever-changing land of software architecture, especially within Android development, the Model-View-Intent (MVI) pattern has emerged as a compelling paradigm, promising a unidirectional and cyclical data flow that simplifies the management of complex user interfaces and state. Yet, despite its advantages, MVI often entangles itself in the pre-existing web of Android architectural patterns, leading to confusion and misapplication. The overlap with the acronym for Model-View-ViewModel (MVVM), a cornerstone of Android development, only adds to the ambiguity.

Recognizing these challenges, our team embarked on a quest to refine the principles of MVI into a more Android-friendly format, resulting in what we’ve termed Model-View-ViewModel with Interactions (MVVI). This approach retains the core philosophy of MVI while embracing the widely used MVVM pattern, fostering a harmonious pairing that leverages the strengths of both architectures.

Central to our interpretation is the use of sealed classes, a feature of Kotlin that has become a staple in the Android developer’s toolbox. With these, we’ve encapsulated all possible user interactions with the view, providing a type-safe and exhaustive way to represent the user’s intent. This adaptation not only aligns with the robust type system of Kotlin but also streamlines the development process by clearly defining the communication channels between our application’s components.

Background: The Genesis of MVI

In the evolving world of app development, maintaining a clean, manageable, and scalable codebase is always desirable. Enter Model-View-Intent (MVI), a pattern that emerged as an answer to these challenges, particularly in reactive programming environments. Its core philosophy is based on the unidirectional data flow and a clear separation of concerns, which resonates with the principles of functional programming.

The pattern divides application architecture into three fundamental components:

  • Model — the layer that holds the business logic and the state of the application.
  • View — the passive interface that displays the data and notifies the system of user actions.
  • Intent — the representation of user actions that informs the Model of what needs to be processed.

MVI’s strength lies in its cyclical nature, where each component has a distinct and singular responsibility, creating a loop that starts with the user’s intent, processes it in the model, and reflects the changes back in the view. This straightforward cycle facilitates easier state management and testing, as each part can be developed and debugged independently of the others.

However, as straightforward as it may seem, MVI’s adoption in Android has been met with a degree of skepticism. The overlap in terminology with the Android-centric MVVM pattern can lead to confusion, as both architectures share similar naming conventions but differ in their operations and intentions. Additionally, the unique lifecycle and state management of Android apps requires a more tailored approach than what traditional MVI offers.

It is within this context that our MVVI approach comes into play. By adapting MVI’s core concepts into the more familiar MVVM framework and explicitly defining user interactions, we aim to provide a robust and intuitive architecture that aligns seamlessly with the Android platform’s practices. The subsequent sections will explore how MVVI is conceptually and practically different and how it addresses Android-specific architectural needs, merging the best of MVI with the proven structure of MVVM.

The MVVI Concept: A Singular Pathway for User Intent

Model-View-ViewModel with Interactions (MVVI) stands as an architectural innovation that caters to the specific needs of Android development by refining the MVI architecture into a pattern that resonates with the platform’s characteristics. At the heart of MVVI lies the onInteraction method, a singular entry point that streamlines user interaction with the ViewModel.

This approach simplifies the communication between the View and the ViewModel, ensuring that all user intents are funneled through a single, manageable channel. The onInteraction method accepts a sealed class — a restricted class hierarchy that represents all the possible interactions an application can handle, which we refer to as ViewInteraction. Here's why this design is advantageous:

  1. Type Safety: By using sealed classes, we ensure that the onInteraction function can only accept a finite set of known interactions, reducing the risk of unexpected behaviors or crashes due to incorrect interaction types.
  2. Maintainability: Having a centralized point for handling interactions makes the code easier to maintain and modify. Developers can add new interactions as sealed subclasses without affecting existing logic.
  3. Readability: With all interactions defined in one place, developers can quickly understand what actions are possible within a given view, enhancing the readability of the code.
  4. Unified Logic: The onInteraction entry point allows for a unified place to handle all user actions, making it easier to implement business logic consistently and to test interactions.

In practice, the MVVI pattern transforms the ViewModel into an interaction processor, where each possible interaction is mapped to a corresponding state change in the model. For instance, an interaction could trigger data fetching, start a save operation, or prompt a navigation event. The ViewModel, upon processing the interaction, updates the model, which then reflects the new state onto the View.

Here’s a simplified example of what this might look like in Kotlin:

sealed class ViewInteraction {
object LoadData : ViewInteraction()
data class SaveItem(val item: Item) : ViewInteraction()
data class DeleteItem(val itemId: String) : ViewInteraction()
// ... other interactions
}

class MyViewModel(
private val saveUseCase : SaveItemUseCase,
private val deleteItemUseCase : DeleteItemUseCase,
private val loadItemsUseCase : LoadItemsUseCase
) : ViewModel() {
// The unified interaction handling method
fun onInteraction(interaction: ViewInteraction) {
when (interaction) {
is ViewInteraction.LoadData -> loadItemsUseCase.invoke()
is ViewInteraction.SaveItem -> saveItemUseCase.invoke(interaction.item)
is ViewInteraction.DeleteItem -> deleteItemUseCase.invoke(interaction.itemId)
// Handle other interactions
}
}
// ...
}

By employing MVVI, developers can leverage the structured approach of MVVM while enriching it with more explicit and consolidated handling of user interactions, leading to a scalable and organized architecture that is particularly beneficial for complex Android applications.

Challenges in MVVI: Navigating Business Logic and View Control Logic

Implementing the MVVI architecture in Android applications presents several challenges, particularly when distinguishing between business logic operations and view control logic. The architecture aims to maintain a clean separation of concerns; however, the distinction between triggering a business logic operation (like fetching data or updating a database) and view control logic (like showing a dialog or a toast) can sometimes blur, potentially leading to a cluttered ViewModel.

Here’s how this challenge manifests:

  1. ViewModel Purity: The ViewModel should ideally be free of Android Framework dependencies to keep it testable and pure. However, view-related actions such as launching dialogs or toasts are inherently tied to the View (i.e., Activity or Fragment), causing a dilemma about where to place this logic.
  2. Event Handling: When a user interaction results in both a state change and a view action (like showing a success dialog after data is saved), handling this sequence within the MVVI paradigm can be tricky. Should the ViewModel instruct the View to show the dialog, or should the View observe the change in state and decide to show the dialog itself?
  3. Navigation: Similar to view control logic, navigation events lie somewhere between the View and the ViewModel. Navigation could be seen as a side effect of a business operation, making it unclear whether to handle it within the ViewModel or the View.

I will expound on this topic in a later article since the topic warrants more clarity.

Conclusion: MVVI — Clarifying View Models with Interactions.

As we conclude our exploration of Model-View-ViewModel with Interactions (MVVI), it’s evident that the landscape of Android architecture is not just evolving but also diversifying to accommodate the complex nature of modern app development. MVVI represents a synthesis of the rigorous data handling of MVI with the well-structured approach of MVVM, culminating in a pattern that speaks directly to the strengths and challenges of Android development.

The use of sealed classes to define user interactions with the onInteraction method offers a clear and concise entry point that brings about type safety, maintainability, and readability. By explicitly separating business logic from view control logic, MVVI alleviates the common confusion that arises when developers are faced with intertwining UI events with core data operations.

However, as with any architectural pattern, MVVI is not without its challenges. The delicate balance between state management and view effects necessitates a disciplined approach to maintain a clean ViewModel. The further concepts of using reactive programming to distinguish between ViewStateChange and ViewEffect are not just workarounds but crucial aspects that define the robustness of MVVI.

In essence, MVVI is more than just an architectural pattern; it is a mindset that encourages clarity and purpose in handling user interactions. It prompts us to ask not just “What can our app do?” but also “How does our architecture support what our app does?” By marrying intent with interactions, MVVI positions itself as a powerful tool in the Android developer’s arsenal, capable of tackling the intricacies of app development with precision and grace.

As the Android platform continues to mature, and as we, the developers, continue to refine our craft, MVVI stands as a testament to our collective commitment to building better, more maintainable, and more user-centric applications. It is an open invitation to the community to iterate, adapt, and adopt, fostering a collaborative environment where architectures like MVVI are born and thrive.

We look forward to seeing how MVVI is applied and adapted by the Android community and beyond. May this discussion serve as a catalyst for innovation and a stepping stone toward an even more architecturally sound future for mobile app development.

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--