Search code examples
javaandroidspinnerrace-conditiononcreate

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:

  1. I get the mentorID from the EXTRA that's passed in from the originating Activity.
  2. I access my MentorViewModel and call my getAllMentors() method which returns LiveData> of all mentors in the db.
  3. 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.
  4. I populate my spinner with the information in List full of mentor names.
  5. 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.
  6. If I find a match in that list, I call a getMentorName() method to snag their name as a string.
  7. 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.
  8. 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.


Solution

  • 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);
                    }
                 }
                }
            }
        });