This will be a continuation of the article The Evolution of MV* Patterns in Android: Part 2
MVI (Model-View-Intent) is an architectural pattern that is part of the Unidirectional Data Flow pattern family - an approach to system design in which everything appears as a unidirectional flow of actions and control states. Unlike MVVM, MVI assumes only one data source (single source of truth or SSOT). MVI consists of three components: logic, data, and state (Model); UI layer displaying the state (View) and intention(Intent).
For example, if the user clicks on the “Respond to request” button, the click is converted into an event (Intent) necessary for the Model. In this formula, a request will be made to the server, the resulting result will update the screen state. The UI layer, in accordance with the new state, creates a button and displays text stating that the application has been sent.
Let's move on to the example and the code. The example will show a simple method. Let's take an example from previous articles and the first thing we will write is states and events.
sealed interface Event {
object FetchBooks: Event
}
sealed interface State {
object Loading: State
object Empty: State
data class Success(val books: List<Book> = emptyList()) : State
data class Error(val errorMessage: String)
}
Let's move on to the ViewModel. Here we will also use LiveData
, but keep in mind that you can use Kotlin Flow
.
This is what the MainViewModel looks like now:
class MainViewModel : ViewModel() {
private val booksRepository: BooksRepository = BooksRepositoryImpl()
private val _state = MutableLiveData<State>(State.Empty)
val state: LiveData<State>
get() = _state
fun event(event: Event) {
_state.value = State.Loading
when(event) {
Event.FetchBooks -> {
booksRepository.fetchBooks {
if (it.isNotEmpty())
_state.value = State.Success(books = it)
else
_state.value = State.Empty
}
}
}
}
}
Changed MainFragment:
class MainFragment : Fragment(R.layout.fragment_main) {
private lateinit var booksRV: RecyclerView
private lateinit var progressBar: ProgressBar
private val adapter = BooksListAdapter()
private val mainViewModel: MainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
booksRV = view.findViewById(R.id.books_rv)
progressBar = view.findViewById(R.id.progress_bar)
mainViewModel.event(Event.FetchBooks)
observeLiveData()
}
private fun observeLiveData() {
mainViewModel.state.observe(viewLifecycleOwner) { state ->
when(state) {
is State.Empty -> {
progressBar.visibility = View.GONE
}
is State.Loading -> {
progressBar.visibility = View.VISIBLE
}
is State.Success -> {
progressBar.visibility = View.GONE
adapter.submitList(state.books)
}
is State.Error -> {
Toast.makeText(context, state.errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
companion object {
@JvmStatic
fun newInstance() = MainFragment()
}
}
As a result, we have one input where events are processed and an output with a state.
Pros
Cons
State
become huge and we might want split this State
into smaller ones with extra StateFlows
instead of just using one.