Blog Infos
Author
Published
Topics
, , , ,
Published
Unsplash@tbzr

At Google I/O 2017, Google introduced Architecture Components, a collection of libraries designed to address core challenges in Android development and provide guidance for app architecture. These components, part of the broader Android Jetpack library suite, include tools like Jetpack ViewModel to simplify state management and lifecycle awareness.

Jetpack ViewModel has long been a popular topic of discussion among developers, often serving as a cornerstone for implementing MV* design patterns, such as MVVM or MVI. It is frequently compared to Microsoft’s original MVVM pattern, highlighting its versatility and adaptation for Android app architecture.

Some developers might prefer implementing ViewModels manually, but Jetpack ViewModel remains a great role in many common scenarios due to its seamless integration with various libraries. It works effectively with Hilt for dependency injection into ViewModel constructors, with Jetpack Navigation for scoping ViewModel lifecycles, and with Jetpack Compose for exposing screen UI states to composable functions, making it a cornerstone of modern Android app architecture.

In this article, you’ll explore ViewModel’s inner workings, diving into its internal implementation and understanding how it operates under the hood featured in Dove LetterDove Letter is a subscription repository where you can learn, discuss, and share new insights about Android and Kotlin with industrial Android developer interview questions, tips with code, articles, discussion, and trending news. If you’re interested in joining, be sure to check out “Learn Kotlin and Android With Dove Letter.”

ViewModel and ViewModelImpl

Let’s start by examining the ViewModel class. ViewModel is an abstract class that serves as a base for other classes, offering several key functions to manage UI-related data in a lifecycle-conscious way. Its internal implementation (JVM, Android) is outlined in the following code:

public actual abstract class ViewModel {
/**
* Internal implementation of the multiplatform [ViewModel].
*
* **Why is it nullable?** Since [clear] is final, this method is still called on mock objects.
* In those cases, [impl] is `null`. It'll always be empty though because [addCloseable] and
* [getCloseable] are open so we can skip clearing it.
*/
private val impl: ViewModelImpl?
public actual constructor() {
impl = ViewModelImpl()
}
public actual constructor(viewModelScope: CoroutineScope) {
impl = ViewModelImpl(viewModelScope)
}
public actual constructor(vararg closeables: AutoCloseable) {
impl = ViewModelImpl(*closeables)
}
public actual constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable) {
impl = ViewModelImpl(viewModelScope, *closeables)
}
protected actual open fun onCleared() {}
@MainThread
internal actual fun clear() {
impl?.clear()
onCleared()
}
public actual fun addCloseable(key: String, closeable: AutoCloseable) {
impl?.addCloseable(key, closeable)
}
public actual open fun addCloseable(closeable: AutoCloseable) {
impl?.addCloseable(closeable)
}
public actual fun <T : AutoCloseable> getCloseable(key: String): T? = impl?.getCloseable(key)
}
view raw viewmodel.kt hosted with ❤ by GitHub

You might have noticed that the ViewModel class contains a property of type ViewModelImpl, and most of its functions simply delegate their calls to the corresponding functions in ViewModelImpl. To fully understand how these functions operate, we should examine the internal workings of the ViewModelImpl class:

internal class ViewModelImpl {
private val lock = SynchronizedObject()
/**
* Holds a mapping between [String] keys and [AutoCloseable] resources that have been associated
* with this [ViewModel].
*
* The associated resources will be [AutoCloseable.close] right before the [ViewModel.onCleared]
* is called. This provides automatic resource cleanup upon [ViewModel] release.
*/
private val keyToCloseables = mutableMapOf<String, AutoCloseable>()
private val closeables = mutableSetOf<AutoCloseable>()
@Volatile private var isCleared = false
constructor()
constructor(viewModelScope: CoroutineScope) {
addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
}
constructor(vararg closeables: AutoCloseable) {
this.closeables += closeables
}
constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable) {
addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
this.closeables += closeables
}
/** @see [ViewModel.clear] */
@MainThread
fun clear() {
if (isCleared) return
isCleared = true
synchronized(lock) {
for (closeable in keyToCloseables.values) {
closeWithRuntimeException(closeable)
}
for (closeable in closeables) {
closeWithRuntimeException(closeable)
}
// Clear only resources without keys to prevent accidental recreation of resources.
// For example, `viewModelScope` would be recreated leading to unexpected behaviour.
closeables.clear()
}
}
fun addCloseable(key: String, closeable: AutoCloseable) {
// Although no logic should be done after user calls onCleared(), we will
// ensure that if it has already been called, the closeable attempting to
// be added will be closed immediately to ensure there will be no leaks.
if (isCleared) {
closeWithRuntimeException(closeable)
return
}
val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) }
closeWithRuntimeException(oldCloseable)
}
fun addCloseable(closeable: AutoCloseable) {
// Although no logic should be done after user calls onCleared(), we will
// ensure that if it has already been called, the closeable attempting to
// be added will be closed immediately to ensure there will be no leaks.
if (isCleared) {
closeWithRuntimeException(closeable)
return
}
synchronized(lock) { closeables += closeable }
}
fun <T : AutoCloseable> getCloseable(key: String): T? =
@Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? }
private fun closeWithRuntimeException(closeable: AutoCloseable?) {
try {
closeable?.close()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

Let’s break it down one by one. The ViewModelImpl class contains the following key properties and functions:

  • isCleared: This property holds the state indicating whether the ViewModel has already been cleared. Many function calls depend on this property, as it governs whether certain operations are permissible.
  • closeables and keyToCloseables: These properties manage AutoCloseable instances. closeables is a set, while keyToCloseables is a map, allowing for efficient storage and retrieval of closeable resources.
  • addCloseable(): There are two variations of this function, but their primary purpose is the same — to add an AutoCloseable instance to either the map or set. This is done in a thread-safe manner using a SynchronizedObject, ensuring proper coordination in multi-threaded environments.
  • clear(): This function is responsible for closing all stored AutoCloseable instances from both closeables and keyToCloseables. It also resets the closeables set, ensuring no lingering resources are left behind.
AutoCloseable

Now you might be wondering about the exact purpose of AutoCloseableOriginating from JavaAutoCloseable is a functional interface designed to handle resource management. In Kotlin, AutoCloseable works similarly and allows you to create an instance that executes a specified closeAction when its close() function is called as you’ve seen in the code below:

public actual inline fun AutoCloseable(crossinline closeAction: () -> Unit): AutoCloseable =
java.lang.AutoCloseable { closeAction() }
public interface AutoCloseable {
void close() throws Exception;
}

By now, you should have a clearer understanding of AutoCloseable and its role in resource management. Essentially, the ViewModel maintains a map and a set of AutoCloseable instances, which can be added directly to the ViewModel using the addCloseable() function. When the ViewModel is cleared, all stored AutoCloseable instances are systematically closed by invoking their close() function, ensuring proper cleanup of resources.

So what can you achieve with AutoCloseable? While its use cases may not be extensive, it is particularly useful for delegating tasks that must be executed in the ViewModel‘s onCleared() function. For instance, when working with RxJava or RxKotlin, you need to call dispose() on a CompositeDisposable to clean up all Disposable objects when the ViewModel is cleared.

class MyViewModel : ViewModel() {
private val compositeDisposable = CompositeDisposable()
fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}
override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

No results found.

By utilizing AutoCloseable, you can create a custom class or function to manage CompositeDisposable, ensuring that all disposables are automatically disposed of when the ViewModel is cleared. This approach simplifies resource cleanup and integrates seamlessly with the ViewModel lifecycle.

Ensure that you are using version 2.8.5 or later of the Jetpack Lifecycle library.

class AutoDisposable : AutoCloseable {
private val compositeDisposable = CompositeDisposable()
fun add(disposable: Disposable) {
compositeDisposable.add(disposable)
}
override fun close() {
compositeDisposable.dispose()
}
}
fun ViewModel.autoDisposable(): AutoDisposable {
val autoDisposable = AutoDisposable()
addCloseable(autoDisposable)
return autoDisposable
}
class MyViewModel : ViewModel() {
private val autoDisposable = autoDisposable()
fun addDisposable(disposable: Disposable) {
autoDisposable.add(disposable)
}
}

At first glance, this approach might seem like over-engineering, as it adds more code compared to manually disposing of the CompositeDisposable. However, consider a scenario where you have 100+ ViewModel classes—this method not only simplifies resource management but also eliminates the risk of forgetting to call the dispose() function in the onCleared() method, ensuring consistent and reliable cleanup across your codebase.

viewModelScope

One of the most common examples of leveraging AutoCloseable is the viewModelScope, which is frequently used in daily development to launch coroutine scopes within a ViewModel. Let’s take a look at the internal implementation of viewModelScope:

public val ViewModel.viewModelScope: CoroutineScope
get() =
synchronized(VIEW_MODEL_SCOPE_LOCK) {
getCloseable(VIEW_MODEL_SCOPE_KEY)
?: createViewModelScope().also { scope ->
addCloseable(VIEW_MODEL_SCOPE_KEY, scope)
}
}
private val VIEW_MODEL_SCOPE_LOCK = SynchronizedObject()

Examining the internal implementation of viewModelScope, it first checks if a ViewModel scope has already been created; if it exists, it retrieves the existing scope. If not, it creates a new ViewModelScope using the createViewModelScope() function. To fully understand this process, it’s essential to delve into what the createViewModelScope() function does.

internal fun createViewModelScope(): CloseableCoroutineScope {
val dispatcher =
try {
Dispatchers.Main.immediate
} catch (_: NotImplementedError) {
// In Native environments where `Dispatchers.Main` might not exist (e.g., Linux):
EmptyCoroutineContext
} catch (_: IllegalStateException) {
// In JVM Desktop environments where `Dispatchers.Main` might not exist (e.g., Swing):
EmptyCoroutineContext
}
return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
}
/**
* [CoroutineScope] that provides a method to [close] it, causing the rejection of any new tasks and
* cleanup of all underlying resources associated with the scope.
*/
internal class CloseableCoroutineScope(
override val coroutineContext: CoroutineContext,
) : AutoCloseable, CoroutineScope {
constructor(coroutineScope: CoroutineScope) : this(coroutineScope.coroutineContext)
override fun close() = coroutineContext.cancel()
}

The createViewModelScope() function returns a coroutine scope called CloseableCoroutineScope, which implements AutoCloseable. This scope ensures that the coroutine context is canceled when the close() function is called.

Internally, the mechanism works as follows: viewModelScope creates a custom coroutine scope that is automatically canceled by leveraging the AutoCloseable interface. This scope is then registered with the ViewModel by adding it through the addCloseable() function, ensuring seamless integration with the ViewModel lifecycle.

Conclusion

In this article, you’ve explored the internal mechanisms of Jetpack ViewModel, gaining a deeper understanding of how it operates, particularly the implementation and functionality of viewModelScope. While the complete ViewModel mechanism is more intricate—encompassing dedicated providers and lifecycle scoping tailored to Android components—the insights shared here should be sufficient for most project requirements. Hopefully, this article has clarified the workings of ViewModel and provided valuable insights into the internal behavior of viewModelScope.

This topic initially has been covered in Dove Letter, a private repository offering daily insights on Android and Kotlin, including topics like Compose, architecture, industry interview questions, and practical code tips. In just 20 weeks since its launch, Dove Letter has surpassed 400 individual subscribers and 12business/lifetime subscribers. If you’re eager to deepen your knowledge of Android, Kotlin, and Compose, be sure to check out ‘Learn Kotlin and Android With Dove Letter’.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu