I am displaying markers from my database on my map, But i have a filter for the user from where he can adjust the time and the markers will show in that time. I want to observe the list that is updated each time when the user changes the time filter. He selects a start time and end time and I query from the database to get a list with that condition .. I want to observe this list? How can I do that?
my ViewModel:
public MediatorLiveData<List<LocationTracking>> fetchData(LiveData<List<LocationTracking>> source){
if (oldSource == null){
oldSource = source;
} else {
list.removeSource(oldSource);
}
list.addSource(source, locationTrackings -> {
list.postValue(locationTrackings);
});
return list;
}
And in my activity I'm doing this:
My callback isn't called for on change when a new value is inserted in my database. Any help would be highly appreciated.
When the user wants to see whole list without time filter.
mViewModel.fetchData(mViewModel.getFilteredLocationList(
mDataRange.get(0).getTimeStamp(),
mDataRange.get(1).getTimeStamp()
)).observe(this,mFilteredListObserver);
When the user selects a time Filter.
mViewModel.fetchData(mViewModel.getFilteredLocationList(
data.getLongExtra(LocationsFilterActivity.START_TIME, 0),
data.getLongExtra(LocationsFilterActivity.END_TIME, 0)
)).observe(this,mFilteredListObserver);
I've struggled with a similar problem. Once you understand the solution it's quite simple. You need to do something like this in your ViewModel:
private MutableLiveData<DataRange> dataRange = new MutableLiveData<DataRange>();
public LiveData<List<LocationTracking>> list = Transformations.switchMap(dataRange,
newDataRangeValue -> getFilteredLocationList(
newDataRangeValue.get(0).getTimeStamp(),
newDataRangeValue.get(1).getTimeStamp())
);
public void onDataRangeChanged(DataRange dataRange) {
this.dataRange.setValue(dataRange);
}
In your Activity observe the list. The switchMap() method will listen for any changes on dataRange so if you do mViewModel.onDataRangeChanged(newDataRange), getFilteredLocationList() will be called again and list will be updated in Activity.
I highly recommend reading this great article.
Related
I used the lifecycle callback onCreate to fetch data like below
mWeOutViewModel.getPlaceListLiveData()
.observe(this, weOutItemViewModels -> {
AppLogger.i(getCustomTag() + "adding items " + weOutItemViewModels.size());
if (weOutItemViewModels != null && weOutItemViewModels.size() > 0)
mWeOutListAdapter.addToExisting(weOutItemViewModels);
});
As you can see the AppLogger output the initial size which is 0 when the fragment is displayed, then I fetch the data and call postValue (setValue crashes the app and it expected because I fetch data from the internet using a background thread). So I call post value like below :
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> oldList = placeMutableLiveData.getValue();
oldList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+oldList.size());
placeMutableLiveData.postValue(oldList);
}
As you can see the other AppLogger before postValue, the size of the list is displayed(not empty), but nothing happens until the app crashes and nothing is shown in the logs. I have no ways of debugging since even on debug mode nothing happens. The post value doesn't trigger the observer.
I initialize the mutableLivedata like this :
private final MutableLiveData<List<WeOutGroupedViewModels>> placeMutableLiveData = new MutableLiveData<>();
and access like this :
public LiveData<List<WeOutGroupedViewModels>> getPlaceListLiveData() {
return placeMutableLiveData;
}
Event when I make the livedata public to access directly the livedata, there is no change (just in case someone thinks that's is where the issue comes from)
Instead of placeMutableLiveData.postValue(oldList);
I recommend using
placeMutableLiveData.postValue(Collections.unmodifiableList(new ArrayList<>(newList));
That way, the next time you access this list, you won't be able to mutate it in place, which is a good thing. You're not supposed to mutate the list inside a reactive state holder (MutableLiveData).
So theoretically it should look like this:
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> newList = new ArrayList<>(placeMutableLiveData.getValue());
newList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+newList.size());
placeMutableLiveData.postValue(Collections.unmodifiableList(newList));
}
I'm working on a simple calorie counter app using two fragments and a ViewModel. I'm a beginner and this is a modification of an app I just created for a course (this app is not a homework assignment). It uses ViewModel and has a fragment that collects user input and a fragment that displays the input as a MutableList of MutableLiveData. I would like for the list screen to initially be empty except for a TextView with instructions, and I'd like the instructions to disappear once an entry has been added to the list. My class instructor told me to use an if-else statement in the fragment with the list to achieve this, but it's not working. He didn't tell me exactly where to put it. I tried a bunch of different spots but none of them worked. I don't get errors - just no change to the visibility of the TextView.
Here is the code for the ViewModel with the list:
val entryList: MutableLiveData<MutableList<Entry>>
get() = _entryList
init {
_entry = MutableLiveData<Entry>()
_entryList.value = mutableListOf()
}
fun addEntry(entryInfo: Entry){
_entry.value = entryInfo
_entryList.value?.add(_entry.value!!)
}
}
And this is the code for the observer in the list fragment:
Observer { entryList ->
val entryListView: View = inflater.inflate(R.layout.fragment_entry_list, null, false)
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.VISIBLE
} else {
entryListView.instructions_text_view.visibility = View.GONE
}
entryList.forEach {entry ->
val view: View = inflater.inflate(R.layout.entry_list_item, null, false)
view.date_entry_text_view.text = String.format(getString(R.string.date), entry.date)
view.calories_entry_text_view.text =
view.line_divider
binding.entryList.addView(view)
}
Thanks for any help.
I guess you are expecting your observer to get notified of the event when you are adding entryInfo to your event list (_entryList.value?.add(_entry.value!!).
But this won't happen as you are just adding an element to the same mutable list, and as the list reference hasn't changed, live data won't emit any update.
To solve this, you have two options.
Create a new boolean live data which controls when to show and hide the info text. Set its initial value to false, and update it to true in addEntry() function.
Instead of updating the same mutable list, create of copy of it, add the element and set the entryList.value equal to this new list. This way your observer will be notified of the new list.
Additionally, its generally not a good practice to expose mutable data unless there is no alternative. Here you are exposing a mutable list of Entry and that too in the form of a mutable live data. Ideally, your should be exposing LiveData<List<Entry>>.
This is one possible implementation of all the points that I mentioned:
private val _entryList = MutableLiveData(listOf<Entry>()) // Create a private mutable live data holding an empty entry list, to avoid the initial null value.
val entryList: LiveData<List<Entry>> = _entryList // Expose an immutable version of _entryList
fun addEntry(entryInfo: Entry) {
_entryList.value = entryList.value!! + entryInfo
}
I haven't used the _entry live data here, but you can implement it the same way.
set your viewModel to observe on entry added.
I think you have gotten your visibility toggle in the your if else blocks wrong.
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.GONE // OR View.INVISIBLE
} else {
entryListView.instructions_text_view.visibility = View.VISIBLE
}
Your Observer should get notified of changes to entryList when _entryList has changed. Make sure you are calling addEntry() function to trigger the notification.
My database query operations can take a long time, so I want to display a ProgressBar while the query is in progress. This is especially a problem when the user changes the sorting options, because it displays the old list for a while until the new list comes in and the RecyclerView is updated. I just don't know where to capture the Loading and Success states for a query like this.
Here's my method for getting the PagedList from the database:
fun getGameList(): LiveData<PagedList<Game>> {
// Builds a SimpleSQLiteQuery to be used with #RawQuery
val query = buildGameListQuery()
val dataSourceFactory: DataSource.Factory<Int, Game> = database.gameDao.getGameList(query)
val data: LiveData<PagedList<Game>> = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE)
.build()
return data
}
And I update my list by observing this:
val games = Transformations.switchMap(gameRepository.sortOptions) {
gameRepository.getGameList()
}
Do I need a custom DataSource and DataSource.Factory? If so, I have no idea where to even begin with that. I believe it would be a PositionalDataSource, but I can't find any examples online for implementing a custom one.
I also tried adapter.registerAdapterDataObserver() on my RecyclerView adapter. This fires various callbacks when the new list data is being displayed, but I can't discern from the callbacks when loading has started and stopped.
I was ultimately able to fix this by observing the games LiveData. However, it wasn't exactly straightforward.
Here's my DatabaseState class:
sealed class DatabaseState {
object Success : DatabaseState()
object LoadingSortChange: DatabaseState()
object Loading: DatabaseState()
}
Capturing the Loading state was easy. Whenever the user updates the sort options, I call a method like this:
fun updateSortOptions(newSortOptions: SortOptions) {
_databaseState.value = DatabaseState.LoadingSortChange
_sortOptions.value = newSortOptions
}
The Success state was the tricky one. Since my sorting options are contained in a separate Fragment from the RecyclerView, the games LiveData observer fires twice upon saving new sort options (once when ListFragment resumes, and then again a bit later once the database query is completed). So I had to account for this like so:
/**
* The observer that triggers this method fires once under normal circumstances, but fires
* twice if the sort options change. When sort options change, the "success" state doesn't occur
* until the second firing. So in this case, DatabaseState transitions from LoadingSortChange to
* Loading, and finally to Success.
*/
fun updateDatabaseState() {
when (databaseState.value) {
Database.LoadingSortChange -> gameRepository.updateDatabaseState(DatabaseState.Loading)
DatabaseState.Loading -> gameRepository.updateDatabaseState(DatabaseState.Success)
}
}
Finally, I needed to make some changes to my BindingAdapter to smooth out some remaining issues:
#BindingAdapter("gameListData", "databaseState")
fun RecyclerView.bindListRecyclerView(gameList: PagedList<Game>?, databaseState: DatabaseState) {
val adapter = adapter as GameGridAdapter
/**
* We need to null out the old list or else the old games will briefly appear on screen
* after the ProgressBar disappears.
*/
adapter.submitList(null)
adapter.submitList(gameList) {
// This Runnable moves the list back to the top when changing sort options
if (databaseState == DatabaseState.Loading) {
scrollToPosition(0)
}
}
}
I have the following code in my activity where I am trying to fetch data from realm and then display it in a list. I have used a listener which works perfectly.
private RealmResults<Data> mData = repo.fetchData();
// internally the call is handled by
realm.where(Data.class).findAllAsync();
mData.addChangeListener(data -> {
// access to the data stored locally
// receive updates in case data changes
// set the data to the list
});
At a later point in the app, I want to also be able to filter the above data based on a user entered search string and receive the results in the same listener. I have tried the following code.
mData = poiRepo.fetchData("query");
That doesn't seem to work, I'm guessing because it returns a new list to which the listener is not attached. Is there a way I can listen for changes in the result of a realm query when the underlying data has not changed or any other way?
What I am trying to achieve.
mData.addChangeListener(data -> {
// single place where filtered data is delivered and sent to recycler view
});
function a(){
repo.fetchData( //filter parameters )
}
function b(){
repo.fetchData( //filter parameters )
}
function c(){
repo.fetchData( //filter parameters )
}
I'm looking for the best implementation pattern in Android to update a list when one of the elements change in a different activity.
Imagine this user journey:
An async process fetches ten (10) contact profiles from a web server. These are placed in an array and an adapter is notified. The ten (10) contact profiles are now displayed in a list.
The user clicks on contact profile five (5). It opens up an activity with details of this contact profile. The user decides they like it and clicks 'add to favourite'. This triggers an async request to the web server that the user has favourited contact profile five (5).
The user clicks back. They are now presented again with the list. The problem is the list is outdated now and doesn't show that profile five (5) is favourited.
Do you:
Async call the web server for the updated data and notify the adapter to refresh the entire list. This seems inefficient as the call for the list can take a couple of seconds.
On favouriting the profile store the object somewhere (perhaps in a singleton service) marked for 'refresh'. OnResume in the List activity do you sniff the variable and update just that element in the list.
Ensure the list array is static available. Update the array from the detail activity. OnResume in the activity always notify the adapter for a refresh.
Ensure the list array and adapter is static available. Update the array and notify the adapter from the detail activity.
Any other options? What is the best design principle for this?
Async call the web server for the updated data and notify the adapter
to refresh the entire list. This seems inefficient as the call for the
list can take a couple of seconds.
As you say, it's very inefficient. Creating an Object is expensive in Android. Creating a List of many object is much more expensive.
On favouriting the profile store the object somewhere (perhaps in a
singleton service) marked for 'refresh'. OnResume in the List activity
do you sniff the variable and update just that element in the list.
This is not a good solution because there is a probability that the app crashes before we refresh the object or the app get killed by the device.
Ensure the list array is static available. Update the array from the
detail activity. OnResume in the activity always notify the adapter
for a refresh.
Updating the array via a static method or variable is not a good solution because it makes your detail Activity get coupled with the list. Also, you can't make sure that only the detail activity that change the list if your project get bigger.
Ensure the list array and adapter is static available. Update the
array and notify the adapter from the detail activity.
Same as the above, static variable or object is a no go.
You better use an Event Bus system like EventBus.
Whenever you clicks 'add to favourite' in detail activity, send the async request to update favourite to the web server and also send Event to the list activity to update the specific profile object. For example, if your profile has id "777" and the profile is favourited in detail activity then you need to send the Event something like this in your :
btnFavourite.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Send event when click favourite.
EventBus.getDefault.post(new RefreshProfileEvent(id, true);
}
});
RefreshProfileEvent is a simple pojo:
public class RefreshProfileEvent {
private String id;
private boolean isFavourited;
public RefreshProfileEvent(String id, boolean isFavourited) {
this.id = id;
this.isFavourited = isFavourited;
}
//getter and setter
}
Then you can receive the Event in your list activity to update the selected profile:
public class YourListActivity {
...
#Override
protected onCreate() {
...
EventBus.getDefault().register(this);
}
#Override
protected onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(RefreshProfileEvent event) {
// Refresh specific profile
// For example, your profile is saved in List<Profile> mProfiles
// Search for profile by its id.
for(int i = 0; i < mProfiles.size(); i++) {
if(mProfiles.getId().equals(event.getId()) {
// Refresh the profile in the adapter.
// I assume the adapter is RecyclerView adapter named mAdapter
mProfiles.get(i).isFavourited(true);
mAdapter.notifyItemChanged(i);
// Stop searching.
break;
}
}
}
You don't need to wait for AsyncTask request result returned by the server. Just make the profile favourited first and silently waiting for the result. If your request success, don't do anything. But if the request error, make the profile unfavourited and send unobstructive message like SnackBar to inform the user.
Third option is the best when a user changes the data in detail activity the array should be changed and then when the use returns to main activity call Adapter.notifyDataSetChanged(); will do the trick
For an ArrayAdapter , notifyDataSetChanged only works if you use the add() , insert() , remove() , and clear() on the Adapter.
You can do something like this:
#Override
protected void onResume() {
super.onResume();
Refresh();
}
public void Refresh(){
items = //response....
CustomAdapter adapter = new CustomAdapter(MainActivity.this,items);
list.setAdapter(adapter);
}
On every onResume activity it will refresh the list. Hope it helps you.