Last active
March 21, 2019 05:55
-
-
Save PhongHuynh93/36bb767e6fb5a7e34398b3bdd69b14e2 to your computer and use it in GitHub Desktop.
livedata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
############# | |
############# | |
############# | |
############# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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