Introduction to MVI Architecture Pattern in Android

Jyoti Sheoran
Getir
Published in
4 min readFeb 15, 2023
Photo by Kimon Maritz on Unsplash

MVI stands for Model-View-Intent, and it is an architectural pattern used in Android app development. In this pattern, the user interface (UI) is divided into three parts: the Model, the View, and the Intent.

  • Model: represents the state of the UI and the business logic of the application. It can be thought of as a single source of truth for the UI.
  • View: is responsible for rendering the UI based on the state of the Model. It is a passive component that does not modify the Model directly.
  • Intent: represents the user’s intention to perform an action that affects the Model. It can be thought of as an event that triggers a state change in the Model.

The MVI pattern promotes unidirectional data flow, which means that the data flows in one direction: from the Intent to the Model, and then to the View. This helps to ensure that the UI remains consistent and that there are no unexpected changes in the state of the application.

MVI is often used in combination with reactive programming libraries such as RxJava or Kotlin Coroutines, which can simplify the handling of asynchronous events and data streams in the application.

Here is an example of how to implement the MVI (Model-View-Intent) pattern in an Android app using Jetpack Compose, channels, and flows:

  1. Model: First, we define the model for the data in our app. In this example, we want to display a list of posts, each with a title and a body. We’ll use the following data class to represent a post:
data class Post(val id: Int, val title: String, val body: String)

Next, we create a sealed class called PostsState to represent the different states of the UI. In this example, we have three states: Loading, Success (with a list of posts), and Error.

sealed class PostsState {
object Loading: PostsState()
data class Success(val posts: List<Post>): PostsState()
data class Error(val message: String): PostsState()
}

2. View: Next, we define the Composable function that represents the view. In this example, we want to display a list of posts. Here’s what the Composable might look like:

@Composable
fun PostsList(postsState: PostsState, onRefresh: () -> Unit) {
when (postsState) {
is PostsState.Loading -> {
// Show a loading indicator
CircularProgressIndicator()
}
is PostsState.Success -> {
// Show the list of posts
LazyColumn {
items(postsState.posts) { post ->
PostItem(post)
}
}
}
is PostsState.Error -> {
// Show an error message
Text(postsState.message)
}
}
// Add a refresh button at the top of the list
IconButton(onClick = onRefresh) {
Icon(Icons.Filled.Refresh, contentDescription = "Refresh")
}
}

3. Intent: Next, we define the intents that can be sent from the view to the ViewModel. In this example, we only have one intent: RefreshPosts.

sealed class PostsIntent {
object RefreshPosts: PostsIntent()
}

4. ViewModel: Next, we define the ViewModel that will handle the business logic and manage the state of the UI. In this example, we’ll use a channel to receive intents from the view, and a flow to emit the updated state back to the view.

class PostsViewModel {
private val postsChannel = Channel<PostsIntent>(Channel.UNLIMITED)
val postsState: StateFlow<PostsState> get() = _postsState
private val _postsState = MutableStateFlow<PostsState>(PostsState.Loading)

init {
handleIntents()
}

private fun handleIntents() {
viewModelScope.launch {
postsChannel.consumeAsFlow().collect { intent ->
when (intent) {
is PostsIntent.RefreshPosts -> {
// Emit the loading state
_postsState.value = PostsState.Loading
// Make a network request to fetch the posts
val result = fetchPosts()
// Update the state based on the network response
when (result) {
is Result.Success -> {
_postsState.value = PostsState.Success(result.data)
}
is Result.Error -> {
_postsState.value = PostsState.Error(result.message)
}
}
}
}
}
}
}

fun sendIntent(intent: PostsIntent) {
viewModelScope.launch {
postsChannel.send(intent)
}
}
}

We’ll need to define the fetchPosts() function that makes a network request to fetch the posts. For simplicity, we'll use the FakeApi class to simulate the network request.

object FakeApi {
val posts = listOf(
Post(1, "Post 1", "Body 1"),
Post(2, "Post 2", "Body 2"),
Post(3, "Post 3", "Body 3")
)

fun getPosts(): Result<List<Post>> {
return Result.Success(posts)
}
}

Now we can update the handleIntents() function to call fetchPosts() and update the state accordingly.

private fun handleIntents() {
viewModelScope.launch {
postsChannel.consumeAsFlow().collect { intent ->
when (intent) {
is PostsIntent.RefreshPosts -> {
// Emit the loading state
_postsState.value = PostsState.Loading
// Make a network request to fetch the posts
val result = fetchPosts()
// Update the state based on the network response
when (result) {
is Result.Success -> {
_postsState.value = PostsState.Success(result.data)
}
is Result.Error -> {
_postsState.value = PostsState.Error(result.message)
}
}
}
}
}
}
}

private suspend fun fetchPosts(): Result<List<Post>> {
// Simulate a network request
delay(1000)
return FakeApi.getPosts()
}

5. Binding: Finally, we create a Composable that binds the view and ViewModel together and a MainActivity that displays the Composable.

@Composable
fun PostsScreen(viewModel: PostsViewModel = viewModel()) {
val postsState by viewModel.postsState.collectAsState()
val scaffoldState = rememberScaffoldState()

Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(title = { Text("Posts") })
},
content = {
PostsList(
postsState = postsState,
onRefresh = { viewModel.sendIntent(PostsIntent.RefreshPosts) }
)
}
)
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyApplicationTheme {
PostsScreen()
}
}

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// Provide the ViewModel to the PostsScreen Composable
PostsScreen(viewModel = PostsViewModel())
}
}
}
}

That’s it! With this setup, the PostsScreen Composable will display the list of posts and the onRefresh callback will send a RefreshPosts intent to ViewModel, which will update the state of the UI accordingly.

My next article is to showcase how we can implement MVI with reducers. Check that here.

--

--