2

I have a situation where I have a view model for a page that is rendered based on some input data (id) with fields that are initialized from my repository but can be edited by the user.

My repository function is pretty straightforward:

class AccountsRepository {
  public LiveData<Account> getAccount(long id) {
    return roomDb.accountDao().getAccount(id);
  }
}

I want my view model to be re-used for different accounts so I have a LiveData<Long> that will be set with the account ID.

class EditAccountViewModel {

  private MutableLiveData<Long> accountId = new MutableLiveData<>();

  public void setAccountId(long id) {
    accountId.setValue(id);
  }
}

In my view model, I want to expose a mutable field name that will be bound to an EditText view. This field should be initialized by the data in the repository. If I use a simple non-editable binding, I am able to get one-way databinding working:

class EditAccountViewModel {

  private MutableLiveData<Long> accountId = new MutableLiveData<>();
  public LiveData<String> name;

  EditAccountViewModel() {
    this.name = Transformations.map(
        Transformations.switchMap(accountId, repo::getAccount),
        account -> account.name);
  }
}

However, I cannot bind this using @={viewModel.name} because it complains that it does not know how to set the value. I tried writing a helper class like so that uses an underlying MediatorLiveData to set the value, but it looks like my onChanged callback is never called:

class MutableLiveDataWithInitialValue<T> extends MutableLiveData<T> {
  MutableLiveDataWithInitialValue(LiveData<T> initialValue) {
    MediatorLiveData<T> mediator = new MediatorLiveData<>();
    mediator.addSource(
        initialValue,
        data -> {
          // This never gets called per the debugger.
          mediator.removeSource(initialValue);
          setValue(data);
        });
  }
}

I updated the view model as follows:

class EditAccountViewModel {

  private MutableLiveData<Long> accountId = new MutableLiveData<>();
  public MutableLiveData<String> name;

  EditAccountViewModel() {
    this.name = new MutableLiveDataWithInitialValue<>(
        Transformations.map(
            Transformations.switchMap(accountId, repo::getAccount),
            account -> account.name));
  }
}

However, when I do this my EditText field never sets the value from the database, and this makes sense because the onChanged callback is never called in my MediatorLiveData in MutableLiveDataWithInitialValue.

This seems like a pretty common use case, so I'm wondering what I am screwing up?

sohum
  • 3,207
  • 2
  • 39
  • 63

2 Answers2

0

I was able to workaround this with a very inelegant manner:

class EditAccountViewModel {
  private final Executor ioExecutor;

  public final MutableLiveData<String> name = new MutableLiveData<>();
  public final MutableLiveData<String> someOtherField = new MutableLiveData<>();

  public void setAccountId(long id) {
    ioExecutor.execute(() -> {
      Account account = repo.getAccountSync(id);
      name.postValue(account.name);
      someOtherField.postValue(account.someOtherField);
    });
  }

}

This is fine since only the user can edit the fields once they are initialized. But there are obvious race conditions... e.g. if the database takes too long to rade and the user starts typing a value in before then.

sohum
  • 3,207
  • 2
  • 39
  • 63
0

Your helper class was a good start. Seems like you've forgotten that LiveData objects (including your MediatorLiveData) aren't triggered unless you observe them - that's why your mediator's onChange() method was never called. If you pass observe/unobserve actions to your mediator it works as intended.

Since you're tying your ViewModel to a view it might be a good idea to provide at least some initial value in the constructor like you would do on the regular MutableLiveData (even if that value is shortly replaced by the one coming from LiveData).

Java:

public class MutableLiveDataWithInitialValue<T> extends MutableLiveData<T> {

    private MediatorLiveData<T> mediator = new MediatorLiveData<>();

    public MutableLiveDataWithInitialValue(T initialValue, LiveData<T> delayedInitialValue) {
        super(initialValue);
        mediator.addSource(
                delayedInitialValue,
                data -> {
                    mediator.removeSource(delayedInitialValue);
                    setValue(data);
                });
    }

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        mediator.observe(owner, observer);
        super.observe(owner, observer);
    }

    @Override
    public void observeForever(@NonNull Observer<? super T> observer) {
        mediator.observeForever(observer);
        super.observeForever(observer);
    }

    @Override
    public void removeObserver(@NonNull Observer<? super T> observer) {
        mediator.removeObserver(observer);
        super.removeObserver(observer);
    }

    @Override
    public void removeObservers(@NonNull LifecycleOwner owner) {
        mediator.removeObservers(owner);
        super.removeObservers(owner);
    }
}

Kotlin:

class MutableLiveDataWithInitialValue<T>(initialValue: T, delayedInitialValue: LiveData<T>) : MutableLiveData<T>(initialValue) {

    private val mediator = MediatorLiveData<T>()

    init {
        mediator.addSource(delayedInitialValue) {
            mediator.removeSource(delayedInitialValue)
            value = it
        }
    }

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        mediator.observe(owner, observer)
        super.observe(owner, observer)
    }

    override fun observeForever(observer: Observer<in T>) {
        mediator.observeForever(observer)
        super.observeForever(observer)
    }

    override fun removeObserver(observer: Observer<in T>) {
        mediator.removeObserver(observer)
        super.removeObserver(observer)
    }

    override fun removeObservers(owner: LifecycleOwner) {
        mediator.removeObservers(owner)
        super.removeObservers(owner)
    }
}
Werek
  • 123
  • 7