Skip to content

Instantly share code, notes, and snippets.

@PhongHuynh93
Last active March 21, 2019 05:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save PhongHuynh93/36bb767e6fb5a7e34398b3bdd69b14e2 to your computer and use it in GitHub Desktop.
Save PhongHuynh93/36bb767e6fb5a7e34398b3bdd69b14e2 to your computer and use it in GitHub Desktop.
livedata
ARCHITECTURE COMPONENTS - I'M NOT A PURIST BUT ...
http://hannesdorfmann.com/android/arch-components-purist
DIFFERENT LIVEDATA AND OBSERABLE
LiveData seems to be a very simplified version of RxJava’s Observable. LiveData is lifecycle aware so that we as developer don’t have to unsubscribe explicitly as we have to do with RxJava’s Observable. Both implement the Observer pattern.
LiveData doesn’t provide all this fancy functional programming operators as RxJava does although architecture components provides some functional concepts via Transformations helper class such as Transformations.map() and Transformations.switchMap().
https://developer.android.com/topic/libraries/architecture/livedata.html
Unlike a regular observable, LiveData respects the lifecycle of app components, such that the Observer can specify a Lifecycle in which it should observe.
LiveData considers an Observer to be in an active state if the Observer’s Lifecycle is in STARTED or RESUMED state.
-> fragment must extends LifecycleFragment for livedata to has 2 methods onActive() + onInactive()
public class LocationLiveData extends LiveData<Location> {
private LocationManager locationManager;
private SimpleLocationListener listener = new SimpleLocationListener() {
@Override
public void onLocationChanged(Location location) {
setValue(location);
}
};
public LocationLiveData(Context context) {
locationManager = (LocationManager) context.getSystemService(
Context.LOCATION_SERVICE);
}
@Override
protected void onActive() {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
}
@Override
protected void onInactive() {
locationManager.removeUpdates(listener);
}
}
public class MyFragment extends LifecycleFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LiveData<Location> myLocationListener = ...;
Util.checkUserStatus(result -> {
if (result) {
// Notice that the observe() method passes the LifecycleOwner as the first argument.
myLocationListener.observe(this, location -> {
// update UI
});
}
});
}
}
-> extend LifecycleFragment, we will have
- If the Lifecycle is not in an active state (STARTED or RESUMED), the observer isn't called even if the value changes.
- If the Lifecycle is destroyed, the observer is removed automatically.
-> The fact that LiveData is lifecycle-aware provides us a new opportunity: we can share it between multiple activities, fragments, and so on. To keep our example simple, we can make it singleton as follows:
######################
Transformations of LiveData
Sometimes, you may want to make changes to the LiveData value before dispatching it to the observers, or you may need to return a different LiveData instance based on the value of another one.
LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
user.name + " " + user.lastName
});
// DONT DO DID
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository;
}
private LiveData<String> getPostalCode(String address) {
// DON'T DO THIS
return repository.getPostCode(address);
}
}
Android LiveData: hasObservers() vs hasActiveObservers()
https://medium.com/@ephepasha/android-livedata-hasobservers-vs-hasactiveobservers-3fce303ac813
Centralized LiveData with Kotlin 1.2
https://medium.com/@nullthemall/centralized-livedata-with-kotlin-1-2-97bff1b2cb5c
open class BaseMediatorLiveData<T> : MediatorLiveData<T>() {
lateinit var loadingLiveData: MutableLiveData<Int>
lateinit var progressLiveData: MutableLiveData<Int>
lateinit var errorLiveData: MutableLiveData<Throwable>
lateinit var progressObserver: Observer<Int>
lateinit var loadingObserver: Observer<Int>
lateinit var errorObserver: Observer<Throwable>
Using Android Architecture Components with Firebase Realtime Database (Part 1)
############################
old way
public class MainActivity extends AppCompatActivity {
private static final String LOG_TAG = "MainActivity";
private final DatabaseReference ref =
FirebaseDatabase.getInstance().getReference("/hotstock");
@Override
protected void onStart() {
super.onStart();
ref.addValueEventListener(listener);
}
@Override
protected void onStop() {
ref.removeEventListener(listener);
super.onStop();
}
private ValueEventListener listener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// update the UI here with values in the snapshot
String ticker = dataSnapshot.child("ticker").getValue(String.class);
tvTicker.setText(ticker);
Float price = dataSnapshot.child("price").getValue(Float.class);
tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price));
}
@Override
public void onCancelled(DatabaseError databaseError) {
// handle any errors
Log.e(LOG_TAG, "Database error", databaseError.toException());
}
};
}
############################
new way
Extending LiveData with Firebase Realtime Database
public class FirebaseQueryLiveData extends LiveData<DataSnapshot> {
private static final String LOG_TAG = "FirebaseQueryLiveData";
private final Query query;
private final MyValueEventListener listener = new MyValueEventListener();
public FirebaseQueryLiveData(Query query) {
this.query = query;
}
public FirebaseQueryLiveData(DatabaseReference ref) {
this.query = ref;
}
@Override
protected void onActive() {
Log.d(LOG_TAG, "onActive");
query.addValueEventListener(listener);
}
@Override
protected void onInactive() {
Log.d(LOG_TAG, "onInactive");
query.removeEventListener(listener);
}
private class MyValueEventListener implements ValueEventListener {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
setValue(dataSnapshot);
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException());
}
}
}
onActive() and onInactive(): (in the STARTED or RESUMED state), the LiveData object is "active", and the database listener will be added.
-> There's no chance of forgetting to remove the ValueEventListener on the DatabaseReference,
-> If there's a configuration change in the Activity, it will immediately observe the most recent DataSnapshot from the LiveData used in the prior Activity instance. It will not have to wait for another round trip with the server in order to start rendering that data. There's no need to use onSaveInstanceState() with LiveData.
############################
-> we need a ViewModel object to hook that up to the Activity
Because a ViewModel object survives Activity configuration changes (e.g. when the user reorients their device), its LiveData member object will be retained as well.
public class HotStockViewModel extends ViewModel {
private static final DatabaseReference HOT_STOCK_REF =
FirebaseDatabase.getInstance().getReference("/hotstock");
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);
@NonNull
public LiveData<DataSnapshot> getDataSnapshotLiveData() {
return liveData;
}
}
But There's another subtle issue with the fact that each configuration change removes and re-adds the listener.
Each re-add of the listener effectively requires another round trip with the server to fetch the data again, and I'd rather not do that, in order to avoid consuming the user's limited mobile data. Enabling disk persistence helps, but there's a better way (stay tuned to this series for that tip!).
############################
-> used in activity
// Obtain a new or prior instance of HotStockViewModel from the
// ViewModelProviders utility class.
HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class);
LiveData<DataSnapshot> liveData = viewModel.getDataSnapshotLiveData();
liveData.observe(this, new Observer<DataSnapshot>() {
@Override
public void onChanged(@Nullable DataSnapshot dataSnapshot) {
if (dataSnapshot != null) {
// update the UI here with values in the snapshot
String ticker = dataSnapshot.child("ticker").getValue(String.class);
tvTicker.setText(ticker);
Float price = dataSnapshot.child("price").getValue(Float.class);
tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price));
}
}
});
Android Architecture: Communication between ViewModel and View
Aggregating information(loading status, UI state, errors) would help keep the ViewModels lean and clean -> For me status and state
https://android.jlelse.eu/android-architecture-communication-between-viewmodel-and-view-ce14805d72bf
enum class Status {
SUCCESS,
ERROR,
NO_NETWORK,
EMPTY_FIRST_NAME,
EMPTY_LAST_NAME,
EMPTY_CITY,
INVALID_URI
}
#############
EditProfileViewModel.kt
private val status = SingleLiveEvent<Status>()
fun getStatus(): LiveData<Status> {
return status
}
fun handleImage(intent: Intent?) {
intent?.data?.let {
avatar.value = it.toString()
} ?: run { status.value = Status.INVALID_URI }
}
#############
EditProfileFragment.Kt
viewModel.getStatus().observe(this, Observer { handleStatus(it) })
private fun handleStatus(status: Status?) {
when (status) {
Status.EMPTY_FIRST_NAME -> Toast.makeText(activity, "Please enter your first name!", Toast.LENGTH_SHORT).show()
Status.EMPTY_LAST_NAME -> Toast.makeText(activity, "Please enter your last name", Toast.LENGTH_SHORT).show()
Status.EMPTY_CITY -> Toast.makeText(activity, "Please choose your home city", Toast.LENGTH_SHORT).show()
Status.INVALID_URI -> Toast.makeText(activity, "Unable to load the photo", Toast.LENGTH_SHORT).show()
Status.SUCCESS -> {
startActivity(HomeFragment.newIntent(activity))
activity.finish()
}
else -> Toast.makeText(activity, "Something went wrong, please try again!", Toast.LENGTH_SHORT).show()
}
}
#############
#############
#############
#############
LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
* <p>
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
* <p>
* Note that only one observer is going to be notified of changes.
*/
https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Clean, Easy & New- How To Architect Your App: Part 4 — LiveData Transformations
https://proandroiddev.com/clean-easy-new-how-to-architect-your-app-part-4-livedata-transformations-f0fd9f313ec6
map() and switchMap() to easily transform the value received from LiveData,
################
Transformations.map()
LiveData<List<VenueViewModel>> getVenues(String location) {
LiveData<List<VenueData>> dataModelsLiveD =
repository.getVenues(location);
viewModelsLiveD =
Transformations.map(dataModelsLiveD,
newData -> createVenuesViewModel(newData));
return venuesViewModel;
}
didn’t need to attach a LifecycleOwner object here,
Only if there’s an active observer attached to the trigger LiveData:
map() will be calculated, with the LifecycleOwner given on the trigger LiveData —
-> that map() is for applying a synchronous action
################
What if I have an asynchronous action to perform?
Transformations.switchMap()
LiveData locationLiveD = ...;
LiveData<List<VenueData>> dataModelsLiveD =
Transformations.switchMap(locationLiveD,
newLocation -> repository.getVenues(newLocation));
-> Whenever a new value is set, the old value’s task won’t be calculated anymore.
-> Note that the Functions in both map() and switchMap() run on the Main Thread,
so no long running operations should be done there!
################
Chaining transformations
//Holds the location in a the form of [lat,lng], as is convenient for the UI:
LiveData<double[]> locationLiveD = ...;
// Creates a string format from the location, as Repository requires
LiveData<String> locationStrLiveD =
Transformations.map(locationLiveD, newLocation ->
String.format("%0s,%1s", newLocation[0], newLocation[1]));
//Gets the venue DataModels at the location from Repository
LiveData<List<VenueData>> dataModelsLiveD = Transformations.switchMap(locationStrLiveD,
newLocationStr -> repository.getVenues(newLocationStr));
//Transforms DataModels to ViewModels
LiveData<List<VenueViewModel>> viewModelsLiveD = Transformations.map(dataModelsLiveD, newData -> createVenuesViewModel(newData));
Architecture Components, LiveData and FusedLocationProvider
https://medium.com/@abangkis/architecture-components-livedata-and-fusedlocationprovider-de46580d0481
public class CurrentLocationListener extends LiveData<Location> {
// singleton and build google api are ommited
@Override
protected void onActive() {
googleApiClient.connect();
}
@Override
protected void onInactive() {
if (googleApiClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(
googleApiClient, this);
}
googleApiClient.disconnect();
}
@Override
public void onConnected(@Nullable Bundle connectionHint) {
Log.d(TAG, "connected to google api client");
Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if (lastLocation != null) {
setValue(lastLocation);
} else {
Log.e(TAG, "onConnected: last location value is NULL");
}
if(hasActiveObservers() && googleApiClient.isConnected()) {
// LocationRequest locationRequest = LocationHelper.createRequest();
LocationRequest locationRequest = LocationRequest.create();
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
}
}
@Override
public void onLocationChanged(Location location) {
Log.d(TAG, "Location changed received: " + location);
setValue(location);
}
@Override
public void onConnectionSuspended(int cause) {
Log.w(TAG, "On Connection suspended " + cause);
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult result) {
Log.e(TAG, "GoogleApiClient connection has failed " + result);
}
}
5 common mistakes when using Architecture Components
https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb
Leaking LiveData observers in Fragments
Reloading data after every rotation
Leaking ViewModels
Exposing LiveData as mutable to Views
Creating ViewModel’s dependencies after every configuration change
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment