1

I am populating a ListView with data fetched from an online service, which provides the data in pages. Rather than providing a 'next page' button, I am attempting to automatically load the next page of data when the user scrolls near the bottom of the page.

Data is fetched using an AsyncTask:

class RecentTracksTask extends AsyncTask<Integer, Void, Void> {

    ArrayList<Track> recentTracks;

    @Override
    protected Void doInBackground(Integer... page) {
        try {
            // Get a page of 15 tracks
            // Simplified - getPage accepts 'page' and 'limit' parameters and returns a Collection<Track>
            recentTracks = new ArrayList<Track>(getPage(page[0], 15));

        } catch () {}

        return null;
    }

    @Override
    protected void onPostExecute(Void result) {
            TracksAdapter mTracksAdapter = new TracksAdapter(getActivity(),
                    recentTracks);

            ListView listView = (ListView) getActivity().findViewById(R.id.listview);
            listView.setAdapter(mTracksAdapter);
            listView.setOnScrollListener(new EndlessScrollListener());

    }
}

And the adapter:

class TracksAdapter extends ArrayAdapter<Track> {
    private final Context context;
    private final ArrayList<Track> tracks;

    public TracksAdapter(Context context,
                         ArrayList<Track> recentTrackArrayList) {
        super(context, 0, recentTrackArrayList);
        this.context = context;
        this.tracks = recentTrackArrayList;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.list_item,
                    parent, false);
        }

        final Track track = tracks.get(position);

        final TextView tv = (TextView) convertView
                .findViewById(R.id.tv);
        // Repeat for other views

        tv.setText(track.getName());
        // Repeat for other views

        return convertView;
    }
}

From here, I've tried this EndlessScrollListener:

class EndlessScrollListener implements AbsListView.OnScrollListener {

    private int visibleThreshold = 5;
    private int currentPage = 0;
    private int previousTotal = 0;
    private boolean loading = true;

    public EndlessScrollListener() {
    }

    public EndlessScrollListener(int visibleThreshold) {
        this.visibleThreshold = visibleThreshold;
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
                         int visibleItemCount, int totalItemCount) {
        if (loading) {
            if (totalItemCount > previousTotal) {
                loading = false;
                previousTotal = totalItemCount;
                currentPage++;
            }
        }
        if (!loading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) {
            // I load the next page of gigs using a background task,
            // but you can call any function here.
            new RecentTracksTask().execute(currentPage + 1);
            loading = true;
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }
}

I tried simplifying the code as much as possible without removing anything useful but I know it's still a little long to read through.

In the fragment's onCreateView, I call new RecentTracksTask().execute(1); which successfully loads the first 15 items into the ListView. When scrolling near the bottom of the ListView, the entire contents are replaced with the second page, and the view is returned to the top of the list. Repeating this just moves me to the top of the list, still displaying the second page.

  • How can I get it so that the nth page of items is inserted at the bottom of the list, rather than replacing the list?
  • How can I stop it from moving the view to the top of the list?

Thanks in advance!

Ellis
  • 3,048
  • 2
  • 17
  • 28
  • why dont you create custom AbstractWindowedCursor and use SimpleCursorAdapter ? – pskink Feb 21 '14 at 21:39
  • I don't know much about cursors. What would be the advantage of using that instead of my method? – Ellis Feb 22 '14 at 14:04
  • you dont need AbsListView.OnScrollListener and you only need to implement on-demand data – pskink Feb 22 '14 at 14:29
  • Well, I don't need OnScrollListener for @Enragemrt's solution either, but I will keep that in mind. Thanks – Ellis Feb 23 '14 at 19:20

1 Answers1

5

First, get rid of the EndlessScrollListener. Then, update your AsyncTask to fetch the data, append it to the already-in-use adapter, and notify the adapter of the data set change. Finally, in your adapter, add a check for if you reach the end of the list and request the next page. I should warn you, however, that you'll want to keep track of your async task and prevent it from being recreated if you scroll the last view into place while it's already loading.

AsyncTask

class RecentTracksTask extends AsyncTask<Integer, Void, Void> {

    ArrayList<Track> recentTracks;

    @Override
    protected Void doInBackground(Integer... page) {
        try {
            // Get a page of 15 tracks
            // Simplified - getPage accepts 'page' and 'limit' parameters and returns a Collection<Track>
            recentTracks = new ArrayList<Track>(getPage(page[0], 15));

        } catch () {}

        return null;
    }

    @Override
    protected void onPostExecute(Void result) {
        ListView listView = (ListView) getActivity().findViewById(R.id.listview);
        ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter();
        if(adapter == null) {
            adapter = new TracksAdapter(getActivity(), recentTracks);
            listView.setAdapter(adapter);
        } else {
            adapter.addAll(recentTracks);
            adapter.notifyDataSetChanged();
        }
    }
}

Adapter:

class TracksAdapter extends ArrayAdapter<Track> {
    private final Context context;
    private final ArrayList<Track> tracks;
    private int currentPage = 0;

    public TracksAdapter(Context context,
                         ArrayList<Track> recentTrackArrayList) {
        super(context, 0, recentTrackArrayList);
        this.context = context;
        this.tracks = recentTrackArrayList;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.list_item,
                    parent, false);
        }

        final Track track = tracks.get(position);

        final TextView tv = (TextView) convertView
                .findViewById(R.id.tv);
        // Repeat for other views

        tv.setText(track.getName());
        // Repeat for other views

        //Check if we're at the end of the list
        if(position == getCount() - 1) {
            currentPage++;
            new RecentTracksTask().execute(currentPage);
        }

        return convertView;
    }
}
Grant Amos
  • 2,256
  • 17
  • 11
  • This line: `ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter();` returns null so calling: `adapter.addAll(recentTracks);` produces an NPE: `java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.ArrayAdapter.addAll(java.util.Collection)' on a null object reference` – Ellis Feb 21 '14 at 21:37
  • You'll have to add an adapter when you create the list elsewhere in code. If you don't want to do that, just check for the null case. I'll update the answer accordingly. – Grant Amos Feb 21 '14 at 21:38
  • 1
    Aha! I think your way seems better than adding the adapter elsewhere. Also, `currentPage` needs to be assigned as 1 not 0 in the Adapter, otherwise page 1 gets loaded twice. I'm confused about you saying I need to keep track of the AsyncTask, though. My understanding is that `onPostExecute` is always executed after `doInBackground`, and the code that updates the adapter is all in `onPostExecute`. So, surely, whenever I can see items the AsyncTask has finished loading all data. Certainly I can't seem to notice anything going wrong when I try to scroll too quickly. Anyway, thanks for your help! – Ellis Feb 22 '14 at 13:52
  • @GrantAmos-Enragedmrt can you help http://stackoverflow.com/questions/28122304/endless-scrolling-listview-not-working –  Jan 30 '15 at 05:09