Skip to content
Shreyas Patil's Blog

๐Ÿ™…โ€โ™‚๏ธ Don't let ViewModel know about framework level dependencies

Cover image for ๐Ÿ™…โ€โ™‚๏ธ Don't let ViewModel know about framework level dependencies

Hey Android developers ๐Ÿ‘‹, Most of todayโ€™s developers are adopting MVVM architecture primarily for Android app development ๐Ÿ‘จโ€๐Ÿ’ป. While adopting these things, anyone can do certain mistakes (which is absolutely fine). In this article, we are gonna see how to follow good practices with ViewModels in Android and how some decisions can make us helpless. Okay, letโ€™s start ๐Ÿƒโ€โ™‚๏ธ.


๐Ÿ‘ Overview

Letโ€™s understand the concept of ViewModel and whatโ€™s its purpose.

๐Ÿคทโ€โ™‚๏ธ Whatโ€™s ViewModel?

In MVVM architecture, it suggests separating the data presentation logic (Views or UI) from the core business logic part of the application. ViewModel is such a component whose work is to execute core business and prepare required data for UI. So that ViewModel can be used by Activities/Fragments.

๐Ÿค” Why ViewModel shouldnโ€™t hold framework references?

As we discussed earlier that ViewModel is lifecycle aware but View is not. Many developers also pass the Activity/View references to the ViewModel which is the most common case that developers make the ViewModel framework aware. But ViewModel should not be treated like this. If the View reference remains in ViewModel and it gets destroyed then ViewModel still hold that reference which may lead to the memory leaks ๐Ÿ˜ฎ. Thus, the purpose for which ViewModel came into the picture is broken ๐Ÿ’”.


๐Ÿฅฝ Continueโ€ฆ

As you can see in the title, Iโ€™ve mentioned โ€œDonโ€™t let ViewModel know about FRAMEWORK LEVEL DEPENDENCIESโ€. So letโ€™s discuss whatโ€™s exactly Framework Level Dependencies.

So as you might be aware of AndroidViewModel and have used already for some rare cases.

Note: AndroidViewModel is a ViewModel that has Application Context awareness.

Now when we are introducing Context in ViewModel, the last point ๐Ÿ”ด mentioned above is breaking. Why? Because Context is a part of Androidโ€™s framework. Then it decreases testability, modularity and maintainability of the codebase. No matter our application is small scale or large scale but having tests is always good because it acts as a safeguard as it gets expanding. That thing is breaking by using AndroidViewModel. Because it doesnโ€™t let us write unit tests for that part of code (integration testing is possible only) โ˜น. After all, everyone knows how tricky it is to control memory leaks from the usage of Context if something goes wrong.

Why do we need AndroidViewModel? ๐Ÿค”

For handling framework supported tasks in ViewModel sometimes we may need it. Example: File management, storage, WorkManager APIs, etc. But itโ€™s completely up to us to implement business without using it.

Letโ€™s see the example implementation to understand the problemsโ€ฆ


๐Ÿ‘จโ€๐Ÿ’ป Example Implementation

Okay, so we have to develop a simple android app that stores a user session and performs simple login/logout actions. Now itโ€™s obvious that weโ€™ll need to use SharedPreferences. So letโ€™s see how we can implement it.

โŒ Example using AndroidViewModel way

Iโ€™m skipping UI code and will be only showing the ViewModel code. So letโ€™s create UserViewModel. Need to inherit AndroidViewModel and create SharedPreferences for storing a user session. Also, letโ€™s implement business for setting a user session.

@HiltViewModel
class UserViewModel @Inject constructor(
    application: Application,
    private val userRepository: UserRepository
) : AndroidViewModel(application) {

    /**
     * A [SharedPreferences] for storing user preferences.
     */
    private val userPreferences = application.getSharedPreferences(
        UserPreferences.NAME,
        Context.MODE_PRIVATE
    )

    // ...
    // OTHER CODE HERE
    // ...

    /**
     * Creates a new user with [name] and [email] and sets active session of that created user.
     */
    fun setUserSession(name: String, email: String) {
        viewModelScope.launch {
            val user = userRepository.add(name, email)

            withContext(Dispatchers.IO) {
                userPreferences.edit {
                    putInt(UserPreferences.Keys.ID, user.id)
                    putString(UserPreferences.Keys.NAME, user.name)
                    putString(UserPreferences.Keys.EMAIL, user.email)
                }
            }

            // Update LiveData/StateFlow/Rx or any other stream
        }
    }

    /**
     * Object holding user preference key details
     */
    private object UserPreferences {
        const val NAME = "user_pref"

        object Keys {
            const val ID = "user_id"
            const val NAME = "user_name"
            const val EMAIL = "user_email"
        }
    }
}

Have you seen this code ๐Ÿ˜•? Letโ€™s discuss key issues with this.

๐Ÿ‘Ž Disadvantages of this approach

Isnโ€™t it painful? ๐Ÿค• Letโ€™s see how can we make it better ๐Ÿฆธโ€โ™‚๏ธ.


โœ” Example using ViewModel way

So after coming from the previous approach, we need a solution that will be modular, easy to maintain, easy to plug whenever needed. So we can extract the logic of session management into a separate class ๐Ÿ˜ƒ.

Letโ€™s create a SessionManager. Create and implement operations/business logic of session management.

@Singleton
interface SessionManager {
    suspend fun getCurrentUser(): User?
    suspend fun setUserSession(user: User)
    suspend fun clear()
}

@Singleton
class DefaultSessionManager @Inject constructor(
    // Provided by Dagger-Hilt injection module
    @UserPreferences private val userPreferences: SharedPreferences
) : SessionManager {

    override suspend fun setUserSession(user: User) = withContext(Dispatchers.IO) {
        userPreferences.edit {
            putInt(Keys.ID, user.id)
            putString(Keys.NAME, user.name)
            putString(Keys.EMAIL, user.email)
        }
    }

    // ...
    // OTHER FUNCTION IMPLEMENTATIONS
    // ...

    /**
     * Object holding user preference key details
     */
    object UserPreferencesDetails {
        const val NAME = "user_pref"

        object Keys {
            const val ID = "user_id"
            const val NAME = "user_name"
            const val EMAIL = "user_email"
        }
    }
}

Now, itโ€™s time to refactor and clean up UserViewModel ๐Ÿงน.

@HiltViewModel
class UserViewModel @Inject constructor(
    private val sessionManager: SessionManager,
    private val userRepository: UserRepository,
    @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher // Injected by Hilt module. Used in testing
) : ViewModel() {

    // ...
    // OTHER CODE HERE
    // ...

    /**
     * Creates a new user with [name] and [email] and sets active session of that created user.
     */
    fun setUserSession(name: String, email: String) {
        viewModelScope.launch(defaultDispatcher) {
            val user = userRepository.add(name, email)
            sessionManager.setUserSession(user)
            // Update LiveData/StateFlow/Rx or any other stream
        }
    }
}

๐Ÿ˜ƒ Can you see the difference? Now it has inherited core ViewModel. Too much code is now removed.

๐Ÿ‘ Advantages of this approach

๐Ÿ‘Ž Disadvantages of this approach

๐Ÿ™„ Obviously NOT ๐Ÿ˜….


๐Ÿงช Testing

After using the best approach, UserViewModel is ready for testing. Letโ€™s write code to test the code ๐Ÿ˜‚.

image.png


Cool! Thatโ€™s all. I hope this article helped to understand the pitfalls of AndroidViewModel.

If you need to have a look at both approaches, refer to this GitHub Repository. Here youโ€™ll find both approaches. Also, if you want to take a look at just diff only โž•โž– then refer to this Pull Request which is a change from the first approach to the second.


Thank you! ๐Ÿ˜€

โ€œSharing is caringโ€


๐Ÿ“š References



Previous Post
Navigating Screens in Jetpack Compose (DevFest India 2021 - Mobile Track)
Next Post
Observing Live connectivity status in Jetpack Compose way!