0

I have a FragmentStatePagerAdapter with a Listview in each page. The FragmentStatePagerAdapter is being refreshed once every second with new data for some of his pages (remember that each page is a listview).

I set the content of the listview with setAdapter:

StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);

The problem is that some pages has a lot of content and a vertical scroll, so every second when listView.setAdapter(adapter); is being called, the scroll of the listview is forced to his upper position. It is a very abnormal and annoying behaviour.

I find this on Stack Overflow: notifyDataSetChanged() makes the list refresh and scroll jumps back to the top

The problem is that these solutions are designed for a normal ViewPager or normal Adapter and can't work with FragmentStatePageAdapter or something, because after trying them I still have the same problem.

This is my adapter for the FragmentStatePagerAdapter:

public class CollectionPagerAdapter extends FragmentStatePagerAdapter {
    public CollectionPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return ObjectFragment.newInstance(position);
    }

    @Override
    public int getCount() {
        return infoTitlesArray.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return infoTitlesArray[position];
    }
}

The ViewPager which has the problem:

<android.support.v4.view.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v4.view.PagerTitleStrip
            android:id="@+id/pager_title_strip"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            style="@style/CustomPagerTitleStrip"/>
    </android.support.v4.view.ViewPager>

Layout of the fragment:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/listview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
</ListView>

The Java code for the fragment:

public static class ObjectFragment extends Fragment implements DataUpdateListener {
    private int mPage;
    private ListView listView;

    public static ObjectFragment newInstance(int page) {
        ObjectFragment objectFragment = new ObjectFragment();
        Bundle args = new Bundle();
        args.putInt("page", page);
        objectFragment.setArguments(args);
        return objectFragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPage = getArguments().getInt("page");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
        ((MainActivity) getActivity()).addDataUpdateListener(this);
        listView = (ListView) rootView;
        
        StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
        listView.setAdapter(adapter);

        return rootView;
    }

    @Override
    public void onDestroyView() {
        ((MainActivity) getActivity()).removeDataUpdateListener(this);
        super.onDestroyView();
    }

    //DataUpdateListener interface methods
    @Override
    public int getPage() {
        return mPage;
    }

    @Override
    public void onDataUpdated(ArrayList<String> data) {
        StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, data);
        listView.setAdapter(adapter);
    }
}

When the content of the fragment (which is a listview) needs to be updated, the method onDataUpdated is called with the new content received in the parameter.

Edit

Adding adapter which generates a crash, using the code samples from Budius:

private static class StableArrayAdapter extends ArrayAdapter<String> {
        HashMap<String, Integer> mIdMap = new HashMap<String, Integer>();

        public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
            super(context, textViewResourceId, objects);
            for (int i = 0; i < objects.size(); ++i) {
                mIdMap.put(objects.get(i), i);
            }
        }

        public void setData(List<String> objects){
            mIdMap.clear();
            for (int i = 0; i < objects.size(); ++i) {
                mIdMap.put(objects.get(i), i);
            }
        }

        @Override
        public long getItemId(int position) {
            String item = getItem(position);
            return mIdMap.get(item);
        }

        @Override
        public boolean hasStableIds() {
            return true;
        }
    }

Edit 2

New adapter trying to use Budius solution. But it does not work. The content is not being refreshed.

    private static class StableArrayAdapter extends ArrayAdapter<String> {
    List<String> objects;

    public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
        super(context, textViewResourceId, objects);
        this.objects = new ArrayList<>();
        this.objects.addAll(objects);
    }

    public void setData(List<String> objects){
        this.objects.clear();
        this.objects.addAll(objects);
    }

    @Override
    public long getItemId(int position) {
        return Math.abs(objects.get(position).hashCode());
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
NullPointerException
  • 36,107
  • 79
  • 222
  • 382

1 Answers1

1

calling setAdapter(adapter) and the list returning to the top is normal and a "expected" behavior. Afterall, it's a new adapter and the framework should not assume that the new adapter have any relation to the old one.

Said that, your solution should still be around notifyDataSetChanged() and the appropriate usage of stableIds. With that in mind, what you should do is simply switch the ArrayList<String> data instead of creating a new one. Something like this:

@Override
public void onDataUpdated(ArrayList<String> data) {
    adapter.setData(data); // internally replaces the old data with this new one
    adapter.notifyDataSetChanged();
}

and then on your implementation of StableArrayAdapter you must override hasStableIds to return true and override getItemId to return a meaningfull value. Considering your data are simply strings, it's a bit tricky but something like Math.abs(value.hashCode) should be enough.

PS.: I stronly recommend using RecyclerView instead of ListView. Personally I don't understand why Google didn't deprecated ListView yet.

edit:

as per comment, sometihng like this:

public class StableArrayAdapter extends /* whatever you're extending */ {

    ... your code as usual

   @Override public boolean hasStableIds() { return true; }
   @Override public long getItemId(int position) {
       return Math.abs(data.get(position).hashCode());
   }
}

with that you're telling the system that the ID is direct related to the data you got and you returning ID that is connected to your data (even if losely connected). Meaning, it will be one ID for one data and a different ID for a different data.

That way on notify the ListView knows to keep te position based on the ID.

edit 2:

I found your error. Your adapter extends from ArrayAdapter. This already have a List<String> internally and when you call getItem(position);, the data comes from that internal List.

The fix is simple.

  • Delete the List<String> objects; and use the internal List.

To update the data you call add, addAll and clear methods from ArrayAdapter (https://developer.android.com/reference/android/widget/ArrayAdapter.html):

something like:

@Override
public void onDataUpdated(ArrayList<String> data) {
    adapter.clear();
    adapter.addAll(data);
    adapter.notifyDataSetChanged();
}
Budius
  • 39,391
  • 16
  • 102
  • 144
  • Hi, thank you for your answer. Please, can you give me a little code example or explanation for this? "and then on your implementation of StableArrayAdapter you must override hasStableIds to return true and override getItemId to return a meaningfull value. Considering your data are simply strings, it's a bit tricky but something like Math.abs(value.hashCode) should be enough." – NullPointerException Sep 26 '16 at 21:00
  • mmmm, i'm getting java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Integer.intValue()' on a null object reference at com.myapp.activities.MainActivity$StableArrayAdapter.getItemId – NullPointerException Sep 27 '16 at 17:47
  • this is my getIdmId method: @Override public long getItemId(int position) { String item = getItem(position); return mIdMap.get(item); } – NullPointerException Sep 27 '16 at 17:48
  • i'm editing my question adding the resulting adapter using your sample code – NullPointerException Sep 27 '16 at 17:52
  • If i remove mIdMap.clear(); from the setData method, then does not crash, but content is not being refreshed... – NullPointerException Sep 27 '16 at 17:55
  • Why are you using this HashMap? It makes no sense. Just have the List in the adapter – Budius Sep 27 '16 at 18:26
  • I get it from a sample code from a tutorial. Now i'm using the List. Please, check my question at the bottom. I added the code. It's not working. The content is not being refreshed. – NullPointerException Sep 27 '16 at 21:17
  • Can u post your new 'onDataUpdate' ? – Budius Sep 28 '16 at 07:21
  • of course, do you mean the onDataUpdated method of the fragment? it's this: @Override public void onDataUpdated(ArrayList data) { adapter.setData(data); } – NullPointerException Sep 28 '16 at 07:27
  • i had notifyDataSetChanged(); there too, but i moved to the setData method of the adapter. None of them works – NullPointerException Sep 28 '16 at 07:57
  • oh! now it works! but i can see a graphic bug with the listview. When comming back to a previous page of the view pager, i can see for half a second a huger list with dummy data, but in half a second the listview deletes it's dummy data and leaves just the correct data. It probably is caused by the reuse of the pages which is doing the viewpager, because there are lists with more size and lists with less size in this viewpager. ¿can this ve solved? – NullPointerException Sep 28 '16 at 09:27
  • ok, it takes half a second to change because i'm changing the data each second. But if i dont change the data, then, the problem is here permanently. Just the first real items from the list are changed but afther them i can see dummy items which are from other page. It's caused by the reutilization of the pages. I hope you know a way to deal with this – NullPointerException Sep 28 '16 at 09:44
  • it seems that the clear() is not working fine or something – NullPointerException Sep 28 '16 at 09:47
  • ok, i solved it simply deleting addAll(objects); from the constructor of the adapter, it seems that calling super with the objects is enought. – NullPointerException Sep 28 '16 at 10:06