Jay Poojara
Android

Android Mavericks(MvRx) Architecture, Android on Autopilot: Getting Started

July 4, 2021 5 min read

author:

Android Mavericks(MvRx) Architecture, Android on Autopilot: Getting Started

Reading Time: 5 minutes

What is Mavericks architecture?

Mavericks is a framework built on top of Jetpack ViewModel. Usually, on ViewModel, we create LiveData and MutableLiveData inside ViewModel, and later on, we expose it to the fragment/activity. But In Mavericks, ViewModel will have only one immutable data class (Known as state class). So, now ViewModel will update only one data class, and it will render the UI.  It’s based on Model-View-ViewModel architecture and is a Kotlin-first and Kotlin-only framework.

Mavericks is based on the concept of React. React is a popular web framework that helps build reactive apps.

In this tutorial, I’ll explain the core concepts of Mavericks, how to use them.

Why Mavericks(MvRx)?

Before going into detail, let’s see its advantage and whether it’s the right fit for your project.

Mavericks Offers

  1. State Management: The state of your app is a data class representing the app’s data properties. For example, In a news app could have news posts.
    Mavericks makes state management very easy and flexible to use.
  2. Easy Integration with Android Architecture components: It is built on top of the android architecture components, making it easy to use with the existing app, which already uses the android architecture component.

Core Concepts

To learn Mavericks, we need to understand only 3 classes: MavericksState, MavericksViewModel, and MavericksView.

MavericksState

In Mavericks, the first step is to create the state of your screen. After that, the MavericksState interface signals the intention of your class to be used as a state.

Modelling a screen using state is useful because it is

  • Thread-safe
  • Easy to read and understand by other developers
  • Easy to test

In Mavericks architecture state should follow the following rules.

  1. The state must be a kotlin data class
  2. The state must have immutable properties
  3. All the properties of the state should have default values.

Derived Properties

Because the state is a Kotlin data class, we can create derived properties. For example

data class UserState(
    val fistName: String = "Jay",
    val lastName: String = "Poojara"
) : MavericksState {
    // Derived Properties.
    val fullName = "$fistName $lastName"
}

Creating derived property like fullName means:

  1. We can observe the changes of derived properties using onEach
  2. It will always be in sync with the state
  3. It makes code more readable
  4. It is straightforward to write the unit test cases

MavericksViewModel

MavericksViewModel is responsible only for two things

  1. Updating the state
  2. Expose the stream of states on which other classes can observe

MavericksView

MavericksView is a place where we actually render the data. It can be a fragment/activity. By implementing MavericksView:

  1. We can access the MavericksViewModel via any of the view model delegates. Doing so will automatically subscribe to changes and calls. invalidate().
  2. Override the invalidate() function. invalidate() is used to redraw the UI on each state change.

Mavericks also support Coroutines, dependency injection libraries like Dagger and Hilt, Jetpack compose, Jetpack Navigation etc.

This tutorial will create a simple app that will generate a random number when the user clicks the button using Mavericks.

Step 1: Create a new project & setup required dependencies

For this project, we will need the following dependencies

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'


    // Constraint Layout
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    // Fragment
    implementation "androidx.fragment:fragment-ktx:1.3.5"

    //mavericks
    implementation 'com.airbnb.android:mavericks:2.3.0'

    // Android Lifecycle
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-common-java8:$archLifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$archLifecycleVersion"
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

Step 2: Create application class and initialize Mavericks

I have created the MavericksDemo class and extended it with the Application class. If you do not initialize this class, then the app will throw java.lang.IllegalStateException.

package com.mavericks.demo

import android.app.Application
import com.airbnb.mvrx.Mavericks

class MavericksDemo: Application()  {
    override fun onCreate() {
        super.onCreate()
        Mavericks.initialize(this)
    }
}

After that, register the class to the AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mavericks.demo">

    <application
        android:name=".MavericksDemo"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MavericksDemo">
        <activity android:name=".activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Step 3: Create a state class

Now, let us create our first state class. State class will have all the properties which will help us to render the UI. Right now, we will need only one property called randomNumber.

package com.mavericks.demo.mavericks.vavericksstate

import com.airbnb.mvrx.MavericksState

data class RandomNumberState(
val randomNumber: Int = 0
): MavericksState

Step 4: Create a ViewModel class

Now, this class will help us to modify/update the state.

package com.mavericks.demo.mavericks.viewmodel

import com.airbnb.mvrx.MavericksViewModel
import com.mavericks.demo.mavericks.vavericksstate.RandomNumberState
import kotlin.random.Random

class RandomNumberViewModel(initialState: RandomNumberState) :
    MavericksViewModel<RandomNumberState>(initialState = RandomNumberState()) {
    fun updateRandomNumber() {
        val randomNumber = Random.nextInt(0, 1000)
        setState { copy(randomNumber = randomNumber) }
    }

}

Here, We have created the method called updateRandomNumber. This method is responsible for generating the new random number and updating the state property called randomNumber.

As you can see here, we cannot mutate the state directly. Instead, we have to use the setState function to update the state, and the reason behind this is that by default state is immutable.

copy comes from the state class because the state is a Kotlin data class. Kotlin data class provides this function by default.

We can update any property of the state class using:

setState { copy(yourProperty = newValue) }

Step 5: Create a fragment and extend it using MavericksView

Here, we will create a fragment called RandomNumberFragment and will extend it using MavericksView. This fragment has one TextView and one Button.

package com.mavericks.demo.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MavericksView
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.mavericks.demo.R
import com.mavericks.demo.databinding.FragmentRandomNumberBinding
import com.mavericks.demo.mavericks.viewmodel.RandomNumberViewModel




class RandomNumberFragment : Fragment(), MavericksView {
    private val binding by lazy { FragmentRandomNumberBinding.inflate(layoutInflater) }
    private val viewModel: RandomNumberViewModel by fragmentViewModel()
    override fun invalidate() {
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    }
}

Step 6: Observe the state and update the UI accordingly

Here, we will implement the state observer, and we will update the UI based on it. To do this, we will use a method called withState.

withState Accesses ViewModel state from a single ViewModel synchronously and returns the result of the block.

So, now invalidate() method will look something like this.

override fun invalidate(): Unit = withState(viewModel) {
binding.textViewRandomNumber.text = getString(R.string.random_number, it.randomNumber)
}

So, our RandomNumberFragment will look something like this.

package com.mavericks.demo.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MavericksView
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.mavericks.demo.R
import com.mavericks.demo.databinding.FragmentRandomNumberBinding
import com.mavericks.demo.mavericks.viewmodel.RandomNumberViewModel




class RandomNumberFragment : Fragment(), MavericksView {
private val binding by lazy { FragmentRandomNumberBinding.inflate(layoutInflater) }
private val viewModel: RandomNumberViewModel by fragmentViewModel()
override fun invalidate(): Unit = withState(viewModel) {
binding.textViewRandomNumber.text = getString(R.string.random_number, it.randomNumber)
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonGenerateRandomNumber.setOnClickListener {
viewModel.updateRandomNumber()
}
}
}

Our final output will look something like this:

You can learn more about Mavericks from its official website https://airbnb.io/mavericks/#/. To know more about AirBnb’s motivation behind developing MvRx, you can watch this video on youtube – https://www.youtube.com/watch?v=Web4xPi2Ga4&t=731s.

Note: The codes presented in the work are available on the master branch of my Github account in the android-mavericks-architecture-demo repository. Feel free to browse through!

Hello, I am Jay Poojara, a professional Android, Laravel, NodeJS, EmberJS & React Native Developer. I am a very enthusiastic learner about the latest web and mobile technologies and a big fan of PS4 and Movies.
Leave a comment

Your email address will not be published. Required fields are marked *