MVI architecture implementation with Kotlin flow, Android
Nowadays, many popular architectures are available in Android app development. Like MVVM, MVP & MVI. which can use for large-scale applications. In this article, we will see the implementation of MVI architecture with Kotlin flow. We will also see what MVI is & why MVI is over other architectures. So let`s start with what is MVI.
What is MVI?
The meaning of MVI is Model-View-Intent where the Model consists of business logic that provides data to the View (UI) based on the requested Intent by the user. & Intents are the actions that will trigger based on user action.
MVI Stands for Model-View-Intent, Let’s understand each of them.
- Model: It is a single source of truth for all states. Model ensures that it will update from only one place which is ViewModel / business logic.
- View: View demands user action requests & renders UI as per observation
- Intent: It’s the user action that the user can perform or requests from the Model. Do not confuse it with Android Implicit and Explicit Intent.
In MVI, State will update from only a single place of the app which is business logic (ViewModel). It will ensure that the State will not change from any other place so State can represent as a sealed class.
The State holds the data and View observes those changes to render UI.
When a user wants to perform any operation then action can send through the intent from View. ViewModel will handle the action and the State will observe from View using Kotlin flow or live data.
Why MVI Over Other architectures?
Data flow in MVI is Unidirectional & Cyclic. Along with better use of separation of concerns, it’s easy to catch and fix the bug. State objects are immutable and able to test all layers of application by writing unit tests.
As per the above flow diagram, when the user wants to perform an action user can request it through Intent. It will pass from Intent to ViewModel. once ViewModel executes that request and prepares data then view can observe it from ViewModel. In this way, MVI is working in a cyclic manner.
Let`s see how to set up the MVI architecture pattern, with the use of Kotlin flow along with Separation of concern.
Implementation
Step 1: ApiService Interface
- Create an ApiService interface. With the API endpoint`s method signature with return response type & URL endpoint. Where API types like GET, POST, PUT, and DELETE.
interface ApiService {
@GET("books")
suspend fun getBooks(): List<BookModel>
}
Step 2: ApiHelper Interface
- Create an ApiHelper interface. It includes method signatures that are available in the ApiService interface. Also, we have to implement it in APIHelperImpl class as well.
interface ApiHelper {
suspend fun getBooks(): List<BookModel>
}
Step 3: ApiHelperImpl Class
- Create ApiHelperImpl class. Then provide Dependency injection(DI) of the ApiService interface as a constructor parameter.
- After that implements ApiHelper interface to override its methods.
- Now, Provide a method body to call/execute API call on each overridden method like below.
class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {
override suspend fun getBooks(): List<BookModel> {
return apiService.getBooks()
}
}
Step 4: MainRepostory class for Repository
- Create MainRepository class and provide (DI) of ApiHelper interface as a constructor parameter.
- Then, Add use cases in form of methods/functions to provide implementation of methods.
- Those methods are available in the ApiHelper interface or ApiHelperImpl class. it will use to call APIs from ViewModel.
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getBooks() = apiHelper.getBooks()
}
Step 5: Intent class
- Create an Intent class with a sealed class, that contains all the actions that the user has to perform from View.
sealed class DashboardIntent {
object FetchBook : DashboardIntent()
object ValidateBook : DashboardIntent()
object DeleteBook : DashboardIntent()
}
Step 6: ViewModel class
- Create a ViewModel class that is associated with UI. And provide the (DI) of MainRepository class as a constructor parameter.
- Create userIntent channel & State MutableStateFlow variable for API call with initial state Idle.
- Then create a Sealed class for its possible State so View can observe & update UI. Like [Idle, Loading, Success, Error].
- From ViewModel, we can start consuming user events through the Kotlin flow collect method. When a user requests any action from View then we receive the event through flow.
- Action will perform & View will update as per the State variable observed with its new value.
class DashboardViewModel(private val repository: MainRepository) : ViewModel() {
val userIntent = Channel<DashboardIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<DashboardState>(DashboardState.Idle)
val state: StateFlow<DashboardState>
get() = _state
sealed class DashboardState {
object idle : DashboardState()
object loading : DashboardState()
data class Success(val book: List<BookModel>) : DashboardState()
data class Error(val error: String?) : DashboardState()
}
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is DashboardIntent.FetchBook -> fetchBook()
is DashboardIntent.ValidateBook -> validateBook()
is DashboardIntent.DeleteBook -> deleteBook()
}
}
}
}
private fun fetchBook() {
viewModelScope.launch {
_state.value = DashboardState.Loading
_state.value = try {
DashboardState.Success(repository.getBooks())
} catch (e: Exception) {
DashboardState.Error(e.localizedMessage)
}
}
}
private fun validateBook() {
// logic to validate the book
}
private fun deleteBook() {
// logic to delete the book
}
}
Step 7: UI Activity class
- From the View class, the user can send an Action using send() method of Kotlin flow from the lifecycle scope.
- Observe the latest State updates using Kotlin flows collect() to update UI.
class DashboardActivity: AppCompatActivity() {
private val viewModel: DashboardViewModel by inject()
private fun observeLiveData(){
lifecycleScope.launch {
viewModel.state.collect {
when(it){
is DashboardState.Idle -> {
Log.d("Worked", "Idle")
}
is DashboardState.Loading -> {
handleProgressbar(View.VISIBLE)
Log.d("Worked", "Loading")
}
is DashboardState.Books -> {
handleProgressbar(View.GONE)
Toast.makeText(applicationContext, "Books ${it.book.size}", Toast.LENGTH_LONG).show()
}
is DashboardState.Error -> {
handleProgressbar(View.GONE)
Log.d("Worked", "Error")
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
observeLiveData()
lifecycleScope.launch{
viewModel.userIntent.send(DashboardIntent.FetchBook)
}
}
fun handleProgressbar(viewVisibillity: Int){
findViewById<ProgressBar>(R.id.apiProgressbar).visibility = viewVisibillity
}
}
In conclusion, the Model-View-Intent (MVI) architecture has emerged as a popular choice for developing robust and scalable Android applications. By separating concerns and enforcing a unidirectional flow of data, MVI enables developers to write code that is easier to maintain, test, and modify. With the growing complexity of Android applications, MVI offers a clear and concise approach to managing state and ensuring consistency across the application. Overall, we can say that MVI is also a powerful architecture from MVX series architectures.
Hurray, That`s it. If you reached here it means you have configured MVI architecture.
I hope you have enjoyed reading this as much as I`ve enjoyed writing it. If you believe this tutorial will help someone, do not hesitate to share! you can Smash the clap up to 50 times, and let other people know about MVI configuration using Kotlin flow.