paint-brush
Evolution MV* Patterns in Android: Part 3by@azamatnurkhojayev
122 reads

Evolution MV* Patterns in Android: Part 3

by Azamat NurkhojayevOctober 4th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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.
featured image - Evolution MV* Patterns in Android: Part 3
Azamat Nurkhojayev HackerNoon profile picture


This will be a continuation of the article The Evolution of MV* Patterns in Android: Part 2


MVI

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.


MVI pattern scheme


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

  • State objects are immutable so it is thread-safe.
  • All actions like state and event are in the same file so it is easy to understand what happens on the screen at one look.
  • Maintaining state is easy.
  • Since data flow is unidirectional, tracking is easy.


Cons

  • It causes a lot of boilerplate.
  • High memory management because we have to create lots of object.
  • Sometimes, we have many views and complicated logics, in this kind of situation State become huge and we might want split this State into smaller ones with extra StateFlows instead of just using one.