DEV Community

Cover image for Create your own Android ViewModel from scratch
Khush Panchal
Khush Panchal

Posted on • Originally published at Medium

Create your own Android ViewModel from scratch

You have used ViewModel many times, but do you truly know what purpose it serves, how it is created and how it survives rotation?
In day-to-day Android work, we usually create ViewModels in different ways:

  • Directly with ViewModelProvider:
val vm = ViewModelProvider(this, factory).get(MyViewModel::class.java)
Enter fullscreen mode Exit fullscreen mode
  • With Hilt (DI)
val vm: MyViewModel = hiltViewModel() 
// Hilt hides the provider way, but internally the idea is still similar
Enter fullscreen mode Exit fullscreen mode

What is the purpose of ViewModel

ViewModel is just a class that:

  • holds UI state (example loading, failure, success state),
  • survives configuration changes (example screen rotation, theme changes),
  • gets cleared only when the screen (like activity or fragment) is actually finished

In this blog, we’ll build a mini version of this system ourselves — so the “magic” becomes clear.

What we want to achieve

Before writing code, let’s define the goals (this keeps the implementation clean):

  • ViewModel should survive configuration changes: ViewModel instance should not be cleared when activity is recreated due to configuration changes like screen rotation.
  • Same ViewModel class can exist in multiple Activities, but should not be shared: Two different Activities can both use SomeViewModel class, but each Activity should have its own separate instance.
  • One Activity can have multiple ViewModels, but only one instance per ViewModel class: Inside a single Activity: We can have HomeViewModel, ProfileViewModel, etc.; But for each ViewModel class, there should be only one instance; And when the Activity is truly finished (not rotation), all its ViewModels should be cleared.

Mental model: how we can achieve this

Think of the whole system as two-level storage

Level 1: Store ViewModels for an Activity

Since a single Activity can have multiple ViewModels, we need a container that can store them all together

So we create a class called ViewModelStore which holds ViewModels in a HashMap:

  • key = canonical(fully qualified including path) name of ViewModel class (e.g,: “com.app.ProfileViewModel”)
  • value = actual ViewModel instance

That way:

  • if Activity asks again for ProfileViewModel, we return the same instance from the map
  • if it doesn’t exist, we create and store it
  • we can clear it when activity is destroyed by calling clear method on map

This solves: “single Activity → multiple ViewModels” and “one instance per ViewModel class”.

Level 2: Store ViewModelStore outside Activity

Now comes the main trick

If ViewModelStore lives inside Activity, it will die whenever Activity dies.

But Activity dies in two situations:

  • Configuration change → should NOT clear ViewModels
  • Actual finish (back press) → should clear ViewModels

So we need to store ViewModelStore outside Activity in something that stays alive across rotation

The simplest “lives-through-the-app” place is the Application class.

So inside Application, we keep another HashMap:

  • key = canonical name of Activity class (example: “com.app.ProfileActivity”)
  • value = the Activity’s ViewModelStore

This solves: “do not lose ViewModels on rotation”.

Let’s dive into some code for creating the core components that will help us create our own ViewModel:

Core Components

MyViewModel

Base contract for ViewModel class.

interface MyViewModel {
    // Called before actually destroying the viewmodel instance
    fun onCleared() {}
}
Enter fullscreen mode Exit fullscreen mode

MyViewModelStore

Class that stores multiple ViewModels in a HashMap where the key is the ViewModel’s canonical name and the value is the ViewModel instance.

class MyViewModelStore {
    private val map = HashMap<String, MyViewModel>()

    fun put(key: String, viewModel: MyViewModel) {
        val oldViewModel = map.get(key)
        oldViewModel?.onCleared()
        map[key] = viewModel
    }

    fun get(key: String): MyViewModel? {
        return map[key]
    }

    fun clear() {
        for (vm in map.values) {
            vm.onCleared()
        }
        map.clear()
    }
}
Enter fullscreen mode Exit fullscreen mode

MyViewModelStoreOwner

The owner contract. Our Activity will implement this so it can provide the correct MyViewModelStore.

interface MyViewModelStoreOwner {
    fun getMyViewModelStore(): MyViewModelStore
}
Enter fullscreen mode Exit fullscreen mode

MyViewModelProvider(+ Factory)

The manager. This is the class that returns the same ViewModel instance if it already exists in the store, or creates a new one using the factory when it doesn’t.

class MyViewModelProvider(
    private val owner: MyViewModelStoreOwner,
    private val factory: MyViewModelFactory
) {
    fun <T : MyViewModel> get(modelClass: KClass<T>): T {
        val key = modelClass.qualifiedName!! // canonical name
        var viewModel = owner.getMyViewModelStore().get(key)
        // we check if viewModel already exists else create new
        if (modelClass.isInstance(viewModel)) {
            return viewModel as T
        } else {
            viewModel = factory.create(modelClass)
            owner.getMyViewModelStore().put(key, viewModel)
        }
        return viewModel as T
    }

    interface MyViewModelFactory {
        fun <T : MyViewModel> create(modelClass: KClass<T>): T
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s dive into the some practical example:

Counter Example

To make this system easy to understand, we’ll use a simple UI: a counter text and a button. The counter value is our “UI state”, and the whole purpose of this demo is to prove:

  • Tap the button → count increases
  • Rotate the screen → count should stay the same
  • Press back / close the Activity → ViewModel should clear.

CounterApplication

Application class that will be holding the ViewModelStore in HashMap where key will be the canonical name of our ViewModelStoreOwner (like activity).

class CounterApplication: Application() {
    private val viewModelStores = HashMap<String, MyViewModelStore>()

    fun getViewModelStore(key: String): MyViewModelStore {
        return viewModelStores.getOrPut(key) { MyViewModelStore() }
    }

    fun clearViewModelStore(key: String) {
        viewModelStores.get(key)?.clear()
        viewModelStores.remove(key)
    }
}
Enter fullscreen mode Exit fullscreen mode

CounterViewModel

It holds the count state.

class CounterViewModel : MyViewModel {
    private var count = 0

    init {
        Log.i("CounterViewModel", "ViewModel created")
    }

    fun getCount(): Int {
        return count
    }

    fun incrementCount() {
        count++
    }

    override fun onCleared() {
        super.onCleared()
        Log.i("CounterViewModel", "ViewModel cleared")
    }
}
Enter fullscreen mode Exit fullscreen mode

CounterViewModelFactory

This will create the ViewModel.

class CounterViewModelFactory: MyViewModelProvider.MyViewModelFactory {
    override fun <T : MyViewModel> create(modelClass: KClass<T>): T {
        return CounterViewModel() as T
    }
}
Enter fullscreen mode Exit fullscreen mode

CounterActivity

Implements MyViewModelStoreOwner that provides ViewModelStore.
Requests the ViewModel through MyViewModelProvider, uses it to render the UI, and clears everything only when the Activity is actually finished (not on configuration changes).

class CounterActivity : AppCompatActivity(), MyViewModelStoreOwner {

    private lateinit var counterTv: TextView
    private lateinit var counterIncreaseBtn: Button

    private val viewModel: CounterViewModel by lazy {
        MyViewModelProvider(this, CounterViewModelFactory()).get(CounterViewModel::class)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
        counterTv = findViewById(R.id.textView)
        counterIncreaseBtn = findViewById(R.id.button)

        counterTv.text = viewModel.getCount().toString()

        counterIncreaseBtn.setOnClickListener {
            viewModel.incrementCount()
            counterTv.text = viewModel.getCount().toString()
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        // check if activity is destroying due to configuration changes
        if (!isChangingConfigurations) {
            (application as CounterApplication).clearViewModelStore(this.javaClass.canonicalName!!)
        }
    }

    override fun getMyViewModelStore(): MyViewModelStore {
        return (application as CounterApplication).getViewModelStore(this@CounterActivity.javaClass.canonicalName!!)
    }
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together (the full picture)

Now our brain can imagine the flow like this:

Activity = ViewModelStoreOwner (as it owns ViewModelStore)

  • It doesn’t create ViewModels directly
  • It only knows how to provide its ViewModelStore

Application = the long-living storage

  • It keeps one ViewModelStore per Activity class
  • Rotation happens → Activity recreates → Application still has the store → ViewModels survive

ViewModelStore (as it store ViewModels) = the per-Activity container

  • It keeps one ViewModel instance per ViewModel class using a map

ViewModelProvider = the manager

  • The provider is the class that “glues everything”
  • It checks if ViewModel exists in the store; if yes → returns it; if no → creates it using a Factory, stores it, and returns it

Factory = the creator

  • Only job: create ViewModels when Provider needs a new one.

Application holds Activity’s ViewModelStore, and ViewModelStore holds Activity’s ViewModels.
Two hashmaps → two levels

And this is how the “magic” works :)

Source Code: GitHub

Contact Me: LinkedIn | Twitter

Happy coding! ✌️

Top comments (0)