How to sort MutableLiveData ArrayList in ViewModel? - java

I have an Android app (Java) that makes an api call for shows. After the shows are returned, I need to filter the show arraylist by season & then by episode. I'm currently sorting the list in my fragment because I haven't found a good solution on how to do this in my viewmodel.
This is my call in my vm:
public MutableLiveData<ArrayList<Titles>> getTitlesListLiveData(){
return repository.getTitlesLiveData();
}
And this is how I'm sorting it in my fragment:
titlesViewModel.titlesListLiveData.observe(getViewLifecycleOwner(), titles -> {
for (Titles title : titles) {
titlesList.add(title);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
titlesList.sort(Comparator.comparing(Titles::getSeasonNumber).thenComparing(Titles::getEpisodeNumber));
}
binding.rvTitles.setAdapter(ShowTitlesAdapter);
ShowTitlesAdapter.setShowTitlesList(titlesList);
});
titlesViewModel.getAllTitles(ShowTag);
I find this but I don't understand how I would do it in Java or in my viewmodel. Can anyone help?
UPDATE:
I tried doing this in my ViewModel & it does not work:
public LiveData<List<String>> seasonsListLiveData;
seasonsListLiveData = Transformations.switchMap(titlesListLiveData, titles-> titles.sort(Comparator.comparing(Titles::getSeasonNumber).thenComparing(Titles::getEpisodeNumber));
The error I get: Required type:
LiveData
<List>
Provided:
LiveData
no instance(s) of type variable(s) Y exist so that void conforms to LiveData
Also tried using Transformations.map() & received the same error.

Just use the LiveData Transformations. In your case it could be something like this:
public LiveData<List<Titles>> getTitlesListLiveData()
{
return Transformations.map( repository.getTitlesLiveData(), titles ->
titles.sort(Comparator.comparing(Titles::getSeasonNumber).thenComparing(Titles::getEpisodeNumber))
);
}
Also you don't have to set the adapter every time the data changes:
binding.rvTitles.setAdapter(ShowTitlesAdapter);
You can set it once, for example in the onCreate (when in Activity) or in onCreateView (when in Fragment). Then when the data changes you just call the setShowTitlesList to change the data in adapter

Related

Should Android listView adapter have api calls

Hi I have a adapter which displays the list of items and all the functions related to setItems, getCount, notifyDataSetChanged.
adapter also has calls to api's through use cases.
Structure is
Adapter -> UseCase -> Repository -> apiLayer
I am aware that fragemnts and activities should not contains calls to api (usecases in my instance).
So should adapter's have api calls (usecases in my instance)
Thanks
R
It is possible, but from a software design point of view I would not recommend it.
The Adapter's responsibility is to connect your Data with the View and make it available to the ListView/RecyclerView. The Adapter should not have any other dependency (knowledge). This will also make it more robust to changes.
So you should consider that only the Activity/Fragment talks to your Presenter and delegates the results from the Presenter to the Adapter.
This will also make (unit) testing (of the Presenter) more easier.
class YourActivity: Activity() {
private val presenter: YourPresenter = // ...
override fun onCreate() {
val adapter: YourAdapter = YourAdapter()
recyclerView.setAdapter(adapter)
val data = presenter.getData()
adapter.submit(data)
}
}
class YourPresenter(private val useCase: UseCase : Presenter() {
fun getData(): List<Data> {
return useCase.fetchData()
}
}

Android Java - Race Condition in OnCreate with two Observers and making lists

sorry if this is a convoluted question. Working on creating an app for a college course and I'm running into (what appears to be) a race condition in my OnCreate method.
TL;DR - sometimes my spinner populates and I can get an index from it. Sometimes it's not populated yet when trying to get a specific index. Details and code below.
The app is a "course scheduler" for a college student.
I'm creating an Activity that displays existing course information and allows you to edit it. In the OnCreate method for this Activity, I am filling a spinner for "Mentors" for the course and a spinner for which "Term" the course belongs in. This information is being pulled from a Room DB.
I have a seperate activity for a new course and for editing a course. For the "new course" activity, everything works fine. I getAllMentors() or getAllTerms() successfully and fill the spinner list.
For the "Edit Course" Activity, there's an extra step involved and it seems to be causing me some issues.
When editing a course, I pass the intent from the originating Activity with all the necessary EXTRAS. This is successful.
In OnCreate for EditCourseActivity, I do the following:
I get the mentorID from the EXTRA that's passed in from the originating Activity.
I access my MentorViewModel and call my getAllMentors() method which returns LiveData> of all mentors in the db.
because it returns LiveData, I use an observer and loop through the LiveData adding the Name of each mentor to a List and the
entire mentor to a List.
I populate my spinner with the information in List full of mentor names.
then I do a for loop, looping through List looking for one that has the same id as what I grabbed form the EXTRA in step 1.
If I find a match in that list, I call a getMentorName() method to snag their name as a string.
I have a methond getIndex(spinner, string) that will loop through the provided spinner, trying to find a match for the string that's
passed in (mentors name) that I grabbed that should match the ID of
the mentor assigned to the course. This method returns index location
of the matched string in the spinner.
I set the spinner selection to the index found.
I do basically the same process for term.
Me being a new developer, I'm not used to OnCreate running the code synchronously.
Because of this, it appears that I have a race condition somewhere between populating the List of mentor names that populates the spinner, and calling my getIndex() method.
Sometimes the spinner is populated and getIndex works properly and sets the correct mentor. Sometimes the spinner is empty and my getIndex() returns -1 (which it should do in a no-find situation) that populates the spinner with the first item in the list (once it's populated).
protected void onCreate(Bundle savedInstanceState) {
//////////////////////////Handling Mentor spinner menu/////////////////////////////////////////////////
int mentorId = courseData.getIntExtra(EXTRA_COURSE_MENTOR_ID, -1);
final ArrayAdapter<String> sp_CourseMentorAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mentorNameList);
sp_CourseMentorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
sp_CourseMentor.setAdapter(sp_CourseMentorAdapter);
final MentorViewModel mentorViewModel = ViewModelProviders.of(this).get(MentorViewModel.class);
//Mentor test = mentorViewModel.getMentorById(mentorId);
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
}
sp_CourseMentorAdapter.notifyDataSetChanged();
}
});
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
Is there a way to get them to run asynchronously? Somehow to get the observer doing my getAllMentors() to complete first and populate the spinner, THEN have the for loop run?
Or a better way to handle this?
Thanks in advance.
Room always runs the code on a separated thread, not the Main/UI thread. You can change that behavior with
allowMainThreadQueries()
after initializating your database. This will make the query run first, populate your list and then run your for-loop code. I do not recommend this approach, since it is a bad practice to make queries on the UI thread.
You have two options:
Change your foor loop to a function and call it after adding the values from the observer:
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
lookForMentor();
}
}
});
private void lookForMentor() {
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
Put the for inside the observer, change the Room DAO to return a List and use LiveData on your own viewmodel:
MentorViewModel.java:
MentorViewModel extends ViewModel {
private MutableLiveData<List<Mentor>> _mentorsLiveData = new MutableLiveData<List<Mentor>>();
public LiveData<List<Mentor>> mentorsLiveData = (LiveData) _mentorsLiveData;
void getAllMentors(){
//room db query
_mentorsLiveData.postValue(mentorsList);
}
}
EditActivity.java:
mentorsViewModel.getAllMentors();
mentorViewModel.mentorsLiveData.observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
mentorsListMentor.addAll(mentorList);
sp_CourseMentorAdapter.notifyDataSetChanged();
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
}
});

Firestore Paging Adapter- How to know if query returns 0 results

I'm using firestore paging adapter to populate my RecyclerView with data from Firestore, if collection in Firestore is empty I would like to show a TextView to inform user about that, if it is not then I would like to populate EecyclerView with data but I don't see a way to do this with Firestore paging adapter because I can't acces data from inside the fragment where I create adapter
Im my Fragment inside onViewCreated
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(2)
.setPageSize(5)
.build()
val options = FirestorePagingOptions.Builder<Question>()
.setLifecycleOwner(viewLifecycleOwner)
.setQuery(FirestoreUtil.myFeedQuery, config, Question::class.java)
.build()
mAdapter = WallFeedRVAdapter(this, options)
WallFeedRVAdapter is RecyclerView adapter where I populate MyViewHolder with loaded data. How can I from this current fragment that hosts RecyclerView know if myFeedQuery returned any results so I can update recyclerView visibility to GONE and emptyInfoTextView to VISIBLE.
To get the number of items that are returned by the query which is passed to the FirestorePagingOptions object, you need to use getItemCount() method that exist in your adapter class. Because the data from Cloud Firestore is loaded asynchronously, you cannot simply call getItemCount() directly in your adapter class, as it will always be zero. So in order to get the total number of items, you need to register an observer like in the following lines of code:
mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
public void onItemRangeInserted(int positionStart, int itemCount) {
int totalNumberOfItems = adapter.getItemCount();
Log.d(TAG, String.valueOf(totalNumberOfItems));
if(totalNumberOfItems == 0) {
recyclerView.setVisibility(View.GONE);
emptyInfoTextView.setVisibility(View.VISIBLE);
}
}
});
I solved this problem by simple code. Just override onLoadingStateChangedListner method inside your FirestorePagingAdapter and then use getItemCount.
I implemented it as follow -
#Override
protected void onLoadingStateChanged(#NonNull LoadingState state) {
switch (state){
case LOADED:
case FINISHED:
progressBar.setVisibility(View.GONE);
if(getItemCount() == 0) {
emptyIcon.setVisibility(View.VISIBLE);
recyclerview.setVisibility(View.GONE);
}
break;
}
super.onLoadingStateChanged(state);
}
solved issue with registerAdapterDataObserver

ViewModel observes LiveData from another ViewModel

I have an Activity, that contains some views and 2 Fragments (for example, TextInputFragment and VoiceInputFragment).
I created the next ViewModels:
ActivityViewModel with void onInput(String value) method
interface InputViewModel with LiveData<String> getInput() method
TextInputViewModel and VoiceInputViewModel as implementation of InputViewModel
Now I want to observe getInput from both fragments and react on them.
I have the next idea:
Activity.onCreate:
ActivityViewModel avm = ViewModelProviders.of(this).get(ActivityViewModel.class);
TextInputViewModel tivm = ViewModelProviders.of(this).get(TextInputViewModel.class);
tivm.getInput().observeForever(avm::onInput);
VoiceInputViewModel vivm = ViewModelProviders.of(this).get(VoiceInputViewModel.class);
vivm.getInput().observeForever(avm::onInput);
Does this idea correct? What happened when configuration change and my ViewModels try to re-observe each other? Is there any solutions?
After reseaching and reading android.arch sources I came to the next solution:
Change type of ActivityViewModel.answer field to MediatorLiveData<String> and add the next method:
public void addAnswerSource(LiveData<String> source) {
answer.removeSource(source); // to prevent throwing "This source was already added with the different observer" exception
answer.addSource(source, s -> answer.setValue(s));
}
Now Activity.onCreate:
ActivityViewModel avm = ViewModelProviders.of(this).get(ActivityViewModel.class);
TextInputViewModel tivm = ViewModelProviders.of(this).get(TextInputViewModel.class);
avm.addAnswerSource(tivm.getInput());
VoiceInputViewModel vivm = ViewModelProviders.of(this).get(VoiceInputViewModel.class);
avm.addAnswerSource(vivm.getInput());

Getting listview ID name

I'm passing a listview in to an onselect but theres a couple of ways it's called from different listviews. So i'm trying to work out which listview is being clicked.
I thought I could do the following however the string thats returned is like com.myapp.tool/id/32423423c (type thing) instead of lvAssets.
Here is what I've got:
#Override
public void onNumberRowSelect(ListView listview, clsNameID stat) {
if(listview.getAdapter().toString().equals("lvGenericAssets")){
} else if(listview.getAdapter().toString().equals("lvAssets")){
} else {
Functions.ShowToolTip(getApplicationContext(),
listview.getAdapter().toString());
}
}
As Emil Adz said in first, you can get the id of your list by calling list.getId();
Then use String idList = getResources().getResourceEntryName(id); and you will be able to get the name of the id you have given to your list
Why wont you just use: list.getId(); if you defined it in the XML file then you should define there an id for you ListView.
If you are doing this from code then you can use the list.setId(); to first set it's id.
Another thing you can do is to add a Tag to your listView: list.setTag("list1");
and latter on distinct this listView using the Tag: list.getTag();

Categories