LiveData and Transformations
Recently I tried my hands on with LiveData. The task was a simple one,
- Use a simple Login screen.
- Take credentials input
- Use LiveData on the form to validate.
- Once validated login.
The login part consisted of the following
- Use a REST api url to get login token.
- Update the UI on the response received.
Now since the going to the REST api involved network operations this couldn’t be done in the main thread. Also I wanted to stick to MVVM so it was the repository object responsible to fetch data.
Thus the task now becomes,
- When the repository has new data, somehow post this information to the ViewModel.
I first tried using two LiveData objects so that posting a value to one will upload this information to ViewModel and to any observer thereon.
Turns out that doesn’t work,
- No Lifecycle Observer reference in the repository.
- Using observeForever always gave me the first result posted.
The actual ways of doing this is via Transformations. With transformations there’s no need to use multiple LiveData objects in ViewModel and you can just trigger the updates from a single postValue. If you’re not using postValue to publish update you probably don’t even require Transformations, unless you return something different from what was posted.
Transformations
Transformations is a way to
- Propagate information upstream / downstream.
- Change the value while propagating it to the observer.
All we need to do is instantiate a single LiveData object, in the repository and from there we returned the Transformation‘s LiveData object. Thus,
package com.estoreorder.data;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Class that requests authentication and user information from the remote data source and
* maintains an in-memory cache of login status and user credentials information.
*/
public class LoginRepository {
private static volatile LoginRepository instance;
private LoginDataSource dataSource;
private MutableLiveData<Result<LoggedInUser>> loginAPIResult;
private LiveData loginAPIViewModelResult;
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
private LoggedInUser user = null;
// private constructor : singleton access
private LoginRepository(LoginDataSource dataSource) {
this.loginAPIResult = new MutableLiveData<>();
this.loginAPIViewModelResult = Transformations.map(this.loginAPIResult,
new Function<Result<LoggedInUser>, Object>() {
@Override
public Object apply(Result<LoggedInUser> input) {
return input;
}
});
this.dataSource = dataSource;
}
public LiveData<Result<LoggedInUser>> getLiveDataForViewModel() {
return this.loginAPIViewModelResult;
}
public static LoginRepository getInstance(LoginDataSource dataSource) {
if (instance == null) {
instance = new LoginRepository(dataSource);
}
return instance;
}
public boolean isLoggedIn() {
return user != null;
}
public void logout() {
user = null;
dataSource.logout();
}
private void setLoggedInUser(LoggedInUser user) {
this.user = user;
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
}
public void login(final String username, final String password) {
// handle login
Result<LoggedInUser> result = null;
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
Result<LoggedInUser> result = null;
result = dataSource.loginToServer(username, password);
loginAPIResult.postValue(result);
}
});
}
}
As can be seen above, the loginAPIResult is the real LiveData object that’s updated. This update is done in another thread like
public void login(final String username, final String password) {
// handle login
Result result = null;
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
Result result = null;
result = dataSource.loginToServer(username, password);
loginAPIResult.postValue(result);
}
});
}
In the above snippet the API is used to fetch the login token from the server which updates the APIResult.
Since the Transformation maps trigger from this MutableLiveData instance the result flows upstream to the one listening on this result in UI, via the ViewModel.
Thus you should always use Transformations instead of using multiple LiveData objects that trigger the update for other exported LiveData objects.