Architecture your android application with MvRx

Architecture your android application with MvRx

MvRx pronounced “mavericks” is a framework made by Airbnb’s developer team that allows you to build android applications using the Model-View-ViewModel architecture and manage the state of your data without getting bored

MvRx is based on Google jetpack (architecture components) and supports Kotlin coroutines, it can be used only with Kotlin.

State

data class PostState(
    val post: Async<List<Post>> = Uninitialized
) : MavericksState

With MvRx each view has a state, which is just a Kotlin data class which extends from MavericksState The state contains all the data that will be loaded by the view as proprieties. Async<*> allows you to easily manage asynchronous sources and allows your state's properties to exist in different states, depending on that of the viewmodel. These states can be :

Uninitialized (often used by default): Represents that there's not a field value yet.

Loading : Represents that the field's value is loading. This is useful when you want to show a progress bar or loading icon.

Success : When the data has successfully loaded and you can display them on the UI

Fail : Indicates that an error occurred in the request, so you can show an error UI.

In the above example the initial value is Uninitialized since it won't have any data.

ViewModel


class PostViewModel(
    initialState: PostState
) : MavericksViewModel<PostState>(initialState), KoinComponent {
    private val repository: PostRepository by inject()

    init {
        getData()
    }

    private fun getData() {
        repository.getPosts().execute {
            copy(post = it)
        }
    }
}

Here our viewmodel will extend from MavericksViewModel which takes the state as parameter. Only the viewmodel can update the state, since Mavericks supports Kotlin coroutines, it provides us the execute operator on kotlin flow in order to convert it content to Async Since the state is immutable to update it we use copy()

Note : Mavericks does not yet support constructor injection with Koin, that's why I use property injection. But I often use it with Hilt.

View

class PostFragment : Fragment(), OnItemClickListener, MavericksView {
    private val binding by lazy { FragmentPostBinding.inflate(layoutInflater) }
    private val adapter by lazy { PostAdapter(this) }

    private val postViewModel: PostViewModel by fragmentViewModel()
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        binding.recyclerView.adapter = adapter
        return binding.root
    }

    override fun onItemClick(post: Post) {
        Toast.makeText(context,post.title,Toast.LENGTH_SHORT).show()
    }

    override fun invalidate(): Unit = withState(postViewModel) {
        binding.progess.isVisible = it.post is Loading
        when (it.post) {
            is Success -> {
                binding.progess.visibility = View.GONE
                adapter.submitList(it.post.invoke())
            }
            is Fail -> {
                Log.e("message", it.post.error.localizedMessage)
            }
        }
    }
}

the view is a Fragment that extends from MavericksView, to initialize the ViewModel we use the delegate by fragmentViewModel()

  private val postViewModel: PostViewModel by fragmentViewModel()

The view must override the invalidate() method and the ViewModel will be used as shown below

 override fun invalidate(): Unit = withState(postViewModel) {
        binding.progess.isVisible = it.post is Loading
        when (it.post) {
            is Success -> {
                binding.progess.visibility = View.GONE
                adapter.submitList(it.post.invoke())
            }
            is Fail -> {
                Toast.makeText( requireContext(), "An error occured", Toast.LENGTH_SHORT).show()
                Log.e("message", it.post.error.localizedMessage)
            }
        }
    }

invalidate() is called each time the state changes. it represents the state and we check if the data is being loaded the state will be Loading. If there is an error (e.g. : server bug) MvRx will trigger an error and the status will be Fail and will contain an error message. Otherwise, in case of success the state is Success in this case all operations can be performed. You should note that it.post type isAsync<Post> and to cast it to Post simply call the invoke() method

The result looks like below

1_bKi92qiAPX9LE7cIRX_cbw.gif

Conclusion

Mavericks is a very powerful framework ! It contains several tools that we will never know how to expose in a single article, I recommend you to read the official doc.