Android Architecture Components: you are probably leaking ViewModel and might not know why

The Guide to app architecture suggests using ViewModel + Repository with LiveData in order to implement the data flow from Repository to ViewModel. If you use this approach or anything similar, there’s a high chance you could be leaking ViewModel. To help you understand and fix this issue, this article will briefly explain what a memory leak is, some of its implications, what happens when a ViewModel is leaked, why it happens and how to fix it for good.

Memory leak and its implications

Memory leak happens when for some reason an unused allocated memory cannot be released. If your application has an object that is no longer used, it should be released by the Garbage Collector (GC) so that its allocated memory can be used again for another purpose. However if someone has a reference to this object and never clears it, the object won’t ever be garbage collected during the application lifetime, which characterizes a memory leak. But how bad is it?

A limited amount of memory is provided to an application and it varies depending on how much RAM the device has. Having too much memory leaks can make the app deplete close to or all of its memory, which could lead to:

  • Lags: with too much memory used, the GC might try to reclaim memory portions through a process that will stop the main thread from 50ms to 100ms. Since it won’t be able to release the memory that was leaked, this will probably happen many times, making the app quite laggy.
  • ANR error (Application Not Responding error): worse than lags, the app might freeze and become unusable.
  • OOM error (Out of Memory error): your app will run out of memory and then crash.
  • Unexpected behavior: if an object lives longer than it should and your app isn’t well structured, this object can keep doing unintended things, which can cause unexpected issues on the app.

As you can see, memory leaks can make things get really ugly. One of the most common things that beginner Android developers end up doing is passing Activity Context to another class and never removing its reference. But how about the ViewModel? You are probably thinking that you would never pass a ViewModel to another class, such as a Repository. Then how is a ViewModel leaked?

Leaking a ViewModel

A pretty usual process that results in leaking a ViewModel is when it uses a Repository to make an async network request and wait for the response, only to do something else afterwards. Based on this info, think about the scenario on Image 1 where we have a MainActivity, a SecondaryActivity + SecondaryViewModel and a Repository that performs the network request. The MainActivity starts the SecondaryActivity which then asks the Repository to perform a request and waits for the response. However, before the request is finished, you go back to the MainActivity and it gets finished when you are there. 

By this time, since SecondaryActivity has been destroyed, SecondaryViewModel should be dead or garbage collected, meaning it shouldn’t receive the request’s response. However if you implement this flow poorly as in Image 1, the ViewModel won’t be deleted and will still receive the response. Why does it happen?

sequence-viewmodel-leaking
IMAGE 1 – Sequence Diagram of a flow that leaks a ViewModel

To illustrate a similar situation, let’s reproduce this flow with a simulated network request with the code below. 

 

object Repository {

   private var requestCallback: (() -> Unit)? = null

   // simulate a network request but in reality just sets the callback for the response

   fun performFakeRequest(callback: () -> Unit) {

       requestCallback = callback

   }

   // simulate when the network request finishes

   fun finishFakeRequest() {

       requestCallback?.invoke()

   }

}

class SecondaryViewModel : ViewModel() {

   fun onPerformFakeRequestClicked() {

       Log.d("SecondaryViewModel", "performing request from view model $this")

       Repository.performFakeRequest {

           Log.d("SecondaryViewModel", "finished request and executed callback on view model $this")

       }

   }

}

class MainActivity : AppCompatActivity() {

   ...

   private fun setupView() {

       ...

       endFakeRequestButton.setOnClickListener {

           Repository.finishFakeRequest()

       }

   }

}

 

In this small code (full code is here) SecondaryViewModel asks the Repository to perform the request and wait for the response through a callback, whereas MainActivity has a button to simulate when the request ends.

After reproducing the steps from Image 1, let’s analyze the memory allocation using the Profiler Tool:

Screenshot of Profiler Tool showing leaked SecondaryViewModel
IMAGE 2 – Screenshot of Profiler Tool showing leaked SecondaryViewModel

As you can see under Live Allocation->Class Name->app heap section, SecondaryViewModel is still allocated. If you click the button from MainActivity that simulates when the request finishes, you’ll notice that the SecondaryViewModel callback will be triggered even though it should be dead. You can try to force garbage collection but it won’t work. SecondaryViewModel will never be garbage collected because it’s been leaked. But how did it happen? Maybe from the SecondaryViewModel$onPerformFakeRequestClicked$1 on Image 2 you already guessed whose fault it is: the lambda expression used as the callback. 

Callback… the villain?

There are many ways to implement the communication between ViewModel and Repository. You could observe forever a LiveData or use LiveData transformations inside the ViewModel, subscribe to observable events using Rx, use callbacks with a Listener interface or lambda expression. Either way, you have to be careful not to leak the ViewModel. 

Unless you are making a synchronous request in ViewModel, it’s hard to think of how  ViewModel would receive data asynchronously without passing its reference to whoever will trigger the callback. It might not be clear to us, but that’s exactly what happens when we use a callback with lambda expression or something similar.

When creating a lambda expression, you can access everything that is in the scope of the enclosing class inside, right? This is possible because the lambda expression has an implicit reference to the enclosing class. And guess what, that’s the source of our leak.

When SecondaryViewModel calls performFakeRequest(callback: () -> Unit), an implicit reference to the ViewModel is passed to the callback and a reference to the same callback is passed to the Repository, which keeps this reference in its lifetime. Do you know what that means? Yep, the ViewModel will live until the app is terminated. Since Repository is a singleton class it won’t be garbage collected during the application lifetime. Consequently, the same will happen for the callback and the ViewModel.

If the Repository wasn’t a Singleton, it probably wouldn’t be that bad, but it would still be a memory leak because the ViewModel reference would persist for as long as the request doesn’t finish, meaning ViewModel also might not be garbage collected when it should – which is also a memory leak. This would be especially bad if instead of a one shot request, the non-singleton Repository keeps receiving some data until you cancel it. So, the right way to fix this is by clearing all ViewModel.

Avoiding leaks

 

The basic idea to fix this memory leak is pretty simple. In our sample code, when the request is finished, the reference to the callback should be cleared (which would then clear the reference to the ViewModel when the callback is garbage collected). In addition to that, when the ViewModel is no longer used, i.e. when it calls the onCleared() function, any reference to it should also be cleared.. Here are some suggestions on how to achieve this:

  • Have a Repository function that clears the reference to the callback. The downside is when you have many callbacks co-existing in the same Repository and you have to clear a specific one. In this case you’ll probably need to map callbacks to ViewModels somehow.
  • If you are using Rx or anything similar that can unsubscribe to data stream, you just need to unregister from it by disposing the Disposable whenever it is necessary (e.g. when onCleared() is called).
  • Similar to Rx’s Disposable, you can implement your own class that is responsible for unsubscribing from a data stream and clearing callback references. For example, if you implement a request that returns Retrofit’s Call, this class can take care of clearing the request and callbacks like this:
import retrofit2.Call

class Request<T>(

   var call: Call<T>,

   var success: ((T) -> Unit)?,

   var failure: ((Throwable?) -> Unit)? = null

) {

   fun dispose() {

       call.cancel()

       success = null

       failure = null

   }

}

 

I implemented a very simple example of this Request class that fixes the ViewModel leak in our sample code. Since there isn’t really any network request being performed, the Request only contains the callback. Then, when SecondaryViewModel calls onClear(), you just need to dispose the request and that’s it, the implicit ViewModel reference can be garbage collected. You can check the full code here.

class Request(

   var callback: (() -> Unit)?

) {

   fun dispose() {

       callback = null

   }

}

object Repository {

   private var request: Request? = null

   // simulate a network request, but in reality just sets the callback for the response

   fun performFakeRequest(callback: () -> Unit): Request =

       Request(callback).apply {

           request = this

       }

   // simulate when the network request finishes

   fun finishFakeRequest() {

       request?.callback?.invoke()

   }

}

class SecondaryViewModel : ViewModel() {

   var request: Request? = null

   fun onPerformFakeRequestClicked() {

       Log.d("SecondaryViewModel", "performing request from view model $this")

       request = Repository.performFakeRequest {

           Log.d("SecondaryViewModel", "finished request and executed callback on view model $this")

       }

   }

   override fun onCleared() {

       super.onCleared()

       request?.dispose()

   }

}

 

Last but not least, remember when I mentioned “unexpected behavior” as one of the consequences of memory leak? Let’s say your app needs to perform a sequence of very important operations related to saving data locally and all of them should be completed. If you implement it in a way that your ViewModel will sequentially ask the Repository to perform all operations, and if suddenly the ViewModel calls onClear() in the middle of this process, the ViewModel shouldn’t yet be garbage collected so that it can finish all operations. As stated by José Alcérreca, this critical logic shouldn’t be in the ViewModel. It should ask the Repository to do all operations only once and forget about it.

Don’t put logic in the ViewModel that is critical to saving clean state or related to data. Any call you make from a ViewModel can be the last one.” 

 

Wrapping up

Leaking ViewModel might not be as bad as leaking Activity Context, which is probably why most people are not even aware they are leaking it and why there isn’t much information about it on the Internet. But on this article you have seen that it sure can cause many problems if you are not careful enough. Nowadays most people uninstall apps after the very first bug or issue they find, and memory leak can cause many of those.

Even though this article is based on the architecture suggested by the Guide to app architecture, there are many situations that can lead to memory leak whether you are using MVVM, MVP, MVC, or no architecture at all. Memory Leak has nothing to do with an architecture. You should try to always avoid them as they can be very harmful.

I hope you find this article useful. I tried to keep it simple and leave references with further information about the subject.

The sample code that I used is available at ViewModelLeak.

 

References

https://android.jlelse.eu/memory-leak-patterns-in-android-4741a7fcb570

https://livebook.manning.com/book/kotlin-in-action/chapter-5/1

https://www.baeldung.com/java-8-lambda-expressions-tips

https://medium.com/tompee/idiomatic-kotlin-lambdas-and-sam-constructors-fe2075965bfb

https://proandroiddev.com/everything-you-need-to-know-about-memory-leaks-in-android-d7a59faaf46a

https://techbeacon.com/app-dev-testing/what-you-need-know-about-android-app-memory-leaks

https://developer.android.com/topic/performance/memory

https://developer.android.com/studio/profile/android-profiler

https://developer.android.com/studio/profile/memory-profiler

https://developer.android.com/reference/android/arch/lifecycle/LiveData#observeforever

https://proandroiddev.com/clean-easy-new-how-to-architect-your-app-part-4-livedata-transformations-f0fd9f313ec6

http://reactivex.io/

https://www.raywenderlich.com/3983802-working-with-rxjava-disposables-in-kotlin

https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html

https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54

https://developer.android.com/topic/libraries/architecture/viewmodel

 

 

About the author.

Diego Oliveira
Diego Oliveira

A computer scientist who always dig deep to find the best solutions, and is in love with mobile development. Also likes playing video games and basketball.