In this post I will show you a simple way to implement the Android MVI ( Model -> View -> Intent ) app architecture.
Android Architectures
MVVM Architecture
I first started with MVVM ( Model -> View -> ViewModel ) architecture. This was easy to understand.
The idea is to access and update a StateFlow in ViewModel to update changes in the UI layer.
The problem I could see if that some apps can quickly evolve. You can start with a single StateFlow to manage then it could multiply. I have done it in some cases and it could get a little messy if you aren't proactive about your code organization. For small projects, in my opinion, MVVM may be the way to go because the setup if fast.
MVI Architecture
Android MVI ( Model -> View -> Intent ) architecture would utilize a single StateFlow with a state object assigned to it. You would create a data object that will do the actions or "intent" to update the StateFlow. If the project has quite of bit of moving parts of composables / Navigation Menus / etc, then MVI may be a better fit. The one downfall for Android MVI is the actions or "intents" data object could grow rapidly. This would just require extra planning to ensure a tight organized coding.
In my research along with trial and error on starting to learn MVI, I came across quite a few posts that explained it. The explanation of the theory made sense to me but I had issues figuring out the proper implementation.
Process
Here I will try to present a simple example of the implementation approach I created when I learned it. The example is a simple form for first and last name. When you type in the first and last name then press the button to submit, there is a 3 second counter delay ( simulating a network call, database operation, etc ) then the Text element above the form will be populated with the names entered.
I also wanted to test another method so I created a button in the form to change the background of the root element to a random color. This is just for a quick visual to ensure operation works without having to look into the logcat.
Android MVI Architecture
Project Structure
Below is the project structure I will show you how to create
ViewModelFactory
First we will create the ViewModelFactory. Creating a ViewModelFactory for your project is in case you need to pass a variable to your ViewModel for use in the init {} function.
class ViewModelFactory() : ViewModelProvider.Factory {
//------------to create a ViewModel. This will ensure same ViewModel is used
//val factory = ViewModelFactory()
//val viewModel = ViewModelProvider(this, factory)[ViewModel::class.java]//by viewModels<ViewModel>()
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if(modelClass.isAssignableFrom(com.itgeek25.samplemvi.viewmodel.ViewModel::class.java)){
com.itgeek25.samplemvi.viewmodel.ViewModel() as T
} else {
throw IllegalArgumentException("ViewModel Not Found!!")
}
}
}Data.kt
Now we will create the DataState in the Data.kt file. This is where you will hold your application states. It will be a single state to reference, unlike in MVVM there could potentially be multiple.
data class DataState(
val isBackgroundChanging : Boolean = false,
val backgroundColor : Color = Color(
Random.nextFloat(),
Random.nextFloat(),
Random.nextFloat(),
1.0f
),
val isUserUpdating : Boolean = false,
val UserData : User = User("John, Doe"),
val isError : Boolean = false
)
data class User(
var first: String = "",
var last: String = ""
)ItemIntent
Now we will create the ItemIntent. This is where we hold reference to the functions for the actions we want or "intent".
sealed interface ItemIntent {
data class updateText(val user: User) : ItemIntent
data class changeBackground(val color : Color) : ItemIntent
data class onError(val error: String) : ItemIntent
}ViewModel
Now we will add the ViewModel to put all this together.
class ViewModel() : ViewModel(){
private val _mvistate : MutableStateFlow<DataState> = MutableStateFlow<DataState>(DataState())
val mvistate : StateFlow<DataState> = _mvistate.asStateFlow()
init{
_mvistate.update { it.copy(UserData = User()) } // defaults to John Doe
}
fun handleEvents(event : ItemIntent) {
when (event) {
is ItemIntent.updateText -> { updateText(event) }
is ItemIntent.changeBackground -> { updateBackground(event) }
is ItemIntent.onError -> { onError() }
}
}
private fun onError() {
}
private fun updateBackground(event: ItemIntent.changeBackground) {
viewModelScope.launch {
_mvistate.update { _mvistate.value.copy(isBackgroundChanging = true) }
delay(1000L) // simulated processing
_mvistate.update { _mvistate.value.copy(backgroundColor = event.color) }
_mvistate.update { _mvistate.value.copy(isBackgroundChanging = false) }
}
}
private fun updateText(event: ItemIntent.updateText) {
viewModelScope.launch {
_mvistate.update { _mvistate.value.copy(isUserUpdating = true) }
delay(3000L) // simulated processing
_mvistate.update { _mvistate.value.copy(UserData = event.user) }
_mvistate.update { _mvistate.value.copy(isUserUpdating = false) }
}
}
}Bonus
I created a simple function for the animated loading circle. This is a overlay with a 0.5f alpha that centers the loading circle in the middle of the screen after the buttons are pressed for either action.
AnimatedCircleLoading.kt
Lets create it in AnimatedCircleLoading.kt.
@Composable
fun AnimatedCircleLoading(visible: Boolean) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color(0.0f, 0f, 0f, 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}MainActivity
Now lets create the UI and put the logic together in the MainActivity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleMVITheme {
val factory = ViewModelFactory()
val viewModel = ViewModelProvider(
this,
factory
)[ViewModel::class.java]
val state = viewModel.mvistate.collectAsStateWithLifecycle()
HomeScreen(state.value, viewModel::handleEvents)
}
}
}
}
@Composable
fun HomeScreen(
state: DataState,
handleEvents: (ItemIntent) -> Unit
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val first = remember {
mutableStateOf("")
}
val last = remember {
mutableStateOf("")
}
Box(modifier = Modifier
.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(color = state.backgroundColor)
) {
Row {
Text(text = "First: ${state.UserData.first}\nLast: ${state.UserData.last}")
}
Row {
TextField(value = first.value, onValueChange = { first.value = it })
}
Row {
TextField(value = last.value, onValueChange = { last.value = it })
}
Row {
Button(onClick = {
handleEvents(
ItemIntent.updateText(
User(
first = first.value,
last = last.value
)
)
)
first.value = "" // reset inpup box
last.value = "" // reset input box
}) {
Text(text = "Set Name")
}
Button(onClick = {
handleEvents(
ItemIntent.changeBackground(
Color(
Random.nextFloat(),
Random.nextFloat(),
Random.nextFloat(),
1.0f
)
)
)
}) {
Text(text = "Change Background")
}
}
}
//if either action can been triggered, then show loading circle
AnimatedCircleLoading(if(state.isUserUpdating || state.isBackgroundChanging) true else false)
}
}
}Hope this was useful.


