Is livedata builder ok for one-shot operations?

Viewed 848

For example, let's say that we have a product catalog view with an option to add product to a cart. Each time when user clicks add to cart, a viewModel method addToCart is called, that could look like this:

//inside viewModel
fun addToCart(item:Item): LiveData<Result> = liveData {
    val result = repository.addToCart(item) // loadUser is a suspend function.
    emit(result)
}


//inside view
addButton.onClickListener = {
     viewModel.addToCart(selectedItem).observe (viewLifecycleOwner, Observer () {
          result -> //show result
    }
}

What happens after adding for example, 5 items -> will there be 5 livedata objects in memory observed by the view?

If yes, when will they be cleanup? And if yes, should we avoid livedata builder for one-shot operations that can be called multiple times?

3 Answers

Your implementation seems wrong! You are constantly returning a new LiveData object for every addToCard function call. About your first question, it's a Yes.

If you want to do it correctly via liveData.

// In ViewModel

private val _result = MutableLiveData<Result>()
val result: LiveData<Result>
   get() = _result;

fun addToCart(item: Item) {
   viewModelScope.launch {
      // Call suspend functions
      result.value = ...
   }
}

// Activity/Fragment

viewModel.result.observe(lifecycleOwner) { result ->
   // Process the result  
   ...
}

viewModel.addToCart(selectedItem)

All you have to do is call it from activity & process the result. You can also use StateFlow for this purpose. It also has an extension asLiveData which converts Flow -> LiveData as well.

According to LiveData implementation of:

    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

a new Observer (wrapper) is added every time you observe a LiveData. Looking at this I would be carefull creating new Observers from a view (click) event. At the moment I can not tell if a Garbage Collector can free this resources.

As @kaustubhpatange mentioned, you should have one LiveData with a state/value that can be changed by the viewModel, with every new result. That LiveData can be observed (once) in your Activity or Fragment onCreate() function:

fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  viewModel.result.observe(lifecycleOwner) { result ->
    // handle the result
  }
}

Using MutableLiveData in your ViewModel, you can mostly create LiveData only once, and populate it later with values from click events, responses etc.

TL;DR

If your operation is One-Shot use Coroutine and LiveData.

If your operation serving with Streams you can use Flow.

For one-shot operations, your approach it's OK. I think with liveData builder there is no any Memory leak. If you use for example private backing property for LiveData and observe an public LiveData it might occurs different behavior like get latest value before assign new value to that.

Related