2

I'm working on a ViewPager fragment that uses FragmentStatePagerAdapter as the adapter. The Adapter's data is a List object that comes from the database, and I am observing the query with MediatorLiveData. MediatorLiveData merges six different lists that I have for one day and turns them into one list that is used by the Adapter.

I want to add an item to the List to a specific index and update the UI dynamically. The observer notifies the adapter when an item is added and the update works fine, however when I try to do it on a specific index, calling notifyDataSetChanged() causes an IndexOutOfBoundsError.

I initialize the adapter with a list as follows:

public MealDetailsPagerAdapter(FragmentManager fm, List<Long> list, long date, MealViewModel vm) {
    super(fm);
    this.mealIdList = list;
    this.date = date;
    this.model = vm;
}

Where MealViewModel is not relevant to this question, it is used on the fragment that the adapter is creating. The list changes are done by another viewmodel.

Here's a code that works correctly:

public void changeItems(List<Long> list, long date) {
    if(this.date != date){
        this.mealIdList = list;
        notifyDataSetChanged();
        return;
    }
    mealIdList.addAll(list);
    mealIdList = removeDuplicates(mealIdList);
    System.out.println(Arrays.toString(mealIdList.toArray()));
    notifyDataSetChanged();
}

And the observer that calls it:

 @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewModel.getMealIdsInDay().observe(getViewLifecycleOwner(), longs -> {
        myAdapter.changeItems(longs, mCurrentIndexDay);
        if(isResumed){
            mViewPager.setCurrentItem(myAdapter.getPageIndexForMealId(mViewModel.getMealId()));
            isResumed=false;
        }
        updateIndicator(mViewPager);
    });
}

Where isResumed is false by default, however if the user adds a new Meal isResumed is changed to true and the viewPager's current position gets changed to the created Meal's position.

However, with the working code, the created Meal's position will always be at the end of the adapter's List because of addAll(). I want to add the meal to a specific position, but if I get the index with mViewPager.getCurrentItem() and send it to the method as follows:

    mealIdList.addAll(index, list);

the addAll itself works, but notifyDataSetChanged() causes an IndexOutOfBoundsError.

Here's the complete stack trace:

    E/AndroidRuntime: FATAL EXCEPTION: main
Process: fi.seehowyoueat.shye.debug, PID: 14148
java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
    at java.util.ArrayList.set(ArrayList.java:453)
    at androidx.fragment.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.java:147)
    at androidx.viewpager.widget.ViewPager.populate(ViewPager.java:1212)
    at androidx.viewpager.widget.ViewPager.setCurrentItemInternal(ViewPager.java:669)
    at androidx.viewpager.widget.ViewPager.setCurrentItemInternal(ViewPager.java:631)
    at androidx.viewpager.widget.ViewPager.dataSetChanged(ViewPager.java:1086)
    at androidx.viewpager.widget.ViewPager$PagerObserver.onChanged(ViewPager.java:3097)
    at androidx.viewpager.widget.PagerAdapter.notifyDataSetChanged(PagerAdapter.java:291)
    at fi.octo3.shye.view.viewpagers.MealDetailsPagerAdapter.changeTheItems(MealDetailsPagerAdapter.java:87)
    at fi.octo3.shye.fragments.MealDetailsFragment.lambda$onActivityCreated$0(MealDetailsFragment.java:223)
    at fi.octo3.shye.fragments.-$$Lambda$MealDetailsFragment$XB4Svnx84FE6kVa5Gzle01e8F3o.onChanged(Unknown Source:4)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
    at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
    at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
    at fi.octo3.shye.models.viewmodel.-$$Lambda$2CouvY7DQv4NA0nk6EMoH6jUavw.onChanged(Unknown Source:4)
    at androidx.lifecycle.MediatorLiveData$Source.onChanged(MediatorLiveData.java:152)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
    at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
    at androidx.lifecycle.LiveData$1.run(LiveData.java:91)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:280)
    at android.app.ActivityThread.main(ActivityThread.java:6748)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Looking at it, it seems that the problem is somehow caused by the LiveData, but I'm not sure how to fix it. If you want to look at the method that updates the MediatorLiveData that I have it's here:

 private void refresh(long key){
    if(groupsAndMealsMap.get(key) != null){
        //remove source and then value from map
        liveDataMerger.removeSource(groupsAndMealsMap.get(key));
        groupsAndMealsMap.remove(key);
        //add new value to map and add it as a source
        groupsAndMealsMap.append(key, mealIdsInGroup);
        liveDataMerger.addSource(groupsAndMealsMap.get(key), liveDataMerger::setValue);
    }
}

refresh() is called by addItem() that gets an updated List of Meals from the database (list being mealIdsInGroup) and liveDataMerger consists of six LiveData> objects.

Any help will be appreciated. Thank you!

EDIT

Here's the addItem() method, as you can see the service executor waits for the operation to be done before moving on to the refresh() method.

public void addItem(Meal meal, long mealGroupId){
    ExecutorService service = Executors.newCachedThreadPool();
    service.execute(() -> {
        setMealId(db.mealDao().insertMealIntoGroup(meal, mealGroupId));
        mealIdsInGroup = db.mealDao().loadMealsWithinGroup(mealGroupId);
    });
    service.shutdown();
    try {
        service.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    refresh(mealGroupId);
}

2 Answers2

0

I suspect that the issue occurs because the insert operation is not over yet.

Use a Handler to put a delay

       new Handler(getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                notifyDataSetChanged();
            }
        }, 500);

500 ms just for testing.

Ferran
  • 1,442
  • 1
  • 6
  • 10
  • The insert is over, otherwise the addItem method wouldn't have a list to add. in my addItem() method i am running ExecutorService to do the insert and select operations in the DB and followed it with try-catch loop to await the service's termination. – Elisa Jalava Apr 12 '19 at 08:48
  • Tried this anyway just now, got an exception: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 2, found: 3 – Elisa Jalava Apr 12 '19 at 09:03
  • What about calling `notifyDataSetChanged()` just after `addAll`, before `removeDuplicates`. If using Handler() put a small time, for ex.50ms.Same result? – Ferran Apr 12 '19 at 09:19
  • Same result, yes. – Elisa Jalava Apr 12 '19 at 09:26
0

Does that issue occurs no matter of what position you're in?

Because if you are in position 0 of the adapter, the adapter didn't created the fragment that's in 3rd position yet.

Remember that the one of the main differences between ViewPagerAdapter and FragmentStatePagerAdapter is that the latter only creates 3 fragments at a time, and if you are in the first position or in the last, the adapter will only contain 2 instances of fragments.

Let me know if this solution helped you!

Daniel Beleza
  • 389
  • 1
  • 15
  • It occurs if the position is less than the last item in the list. I stated in the question that AddAll() without a specified index works completely fine, so adding to the end of the list works even if the index is specified... Also, what is ViewPagerAdapter? Is it a downloadable library? – Elisa Jalava Apr 12 '19 at 09:11
  • Sorry, I meant PagerAdapter. Yes, that makes sense. The thing is, the addAll() will add data to every fragment currently exists in that adapter. For example: If your adapter count is 10 and you are currently in position 1 (fragment 2) , if you do addAll(index = 3, list) it will give you IndexOutOfBoundsError because the adapter didn't create yet that fragment. This is, in that moment your adapter only have fragment 1, 2 and 3. – Daniel Beleza Apr 12 '19 at 09:29
  • The adapter knows that the count is bigger than that, but for performance reasons (that's why FragmentStatePagerAdapter is awesome when you have many fragments) it only creates 2 or 3 maximum. – Daniel Beleza Apr 12 '19 at 09:29
  • But the thing is, I did not try to add to an index that doesn't exist. By debugging I saw that in the line addAll(index, list) index was 1 while the size was 3. It completed that line correctly, and the error comes from notifyDataSetChanged() itself! I put a breakpoint on that line, and after that the crash occurs. – Elisa Jalava Apr 12 '19 at 09:42