title: Android infinite scroll view sometimes starts on second page during pagination (skipping first page sometimes)
body: <p>I am working on an Android app that utilizes an infinite scroll in a recycler view. The app is a news reader app for my college which allows students to read articles from the college news paper. I collect these articles by accessing the REST api of our website. </p>
  
  <p>I have been using an infinite scroll view which I have gotten from here: <a href="https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView" rel="nofollow noreferrer">https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView</a>, but continue to run into the same issue with it. About 1 / 5 times, when I refresh the scroll view it will start from the second page of news results instead of the first. I have done a bunch of research about this but haven't been able to find anything and I have played around with the source code of the endless scroll listener to no avail. </p>
  
  <p>Below I have attached the relevant source code of my project and a series of images showing the issue.</p>
  
  <p>Code:</p>
  
  <p>Infinite scroll listener. This was taken from the github link above.</p>
  
  <pre><code>package hu.ait.macweekly.listeners;
  
  import android.support.v7.widget.GridLayoutManager;
  import android.support.v7.widget.LinearLayoutManager;
  import android.support.v7.widget.RecyclerView;
  import android.support.v7.widget.StaggeredGridLayoutManager;
  
  
  /**
   * Code from https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView
   */
  
  public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
  
      public static final String NO_SEARCH = "";
      public static final int NO_CATEGORY = -1;
  
      // The minimum amount of items to have below your current scroll position
      // before loading more.
      private int visibleThreshold = 25;
      // The current offset index of data you have loaded
      private int currentPage = 0;
      // The total number of items in the dataset after the last load
      private int previousTotalItemCount = 0;
      // True if we are still waiting for the last set of data to load.
      private boolean loading = true;
      // Sets the starting page index
      private int startingPageIndex = 0;
  
  
      // Sets category
      private int categoryId = NO_CATEGORY;
      // Sets search
      private String searchString = NO_SEARCH;
  
      RecyclerView.LayoutManager mLayoutManager;
  
      public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
          this.mLayoutManager = layoutManager;
      }
  
      public int getLastVisibleItem(int[] lastVisibleItemPositions) {
          int maxSize = 0;
          for (int i = 0; i &lt; lastVisibleItemPositions.length; i++) {
              if (i == 0) {
                  maxSize = lastVisibleItemPositions[i];
              }
              else if (lastVisibleItemPositions[i] &gt; maxSize) {
                  maxSize = lastVisibleItemPositions[i];
              }
          }
          return maxSize;
      }
  
      // This happens many times a second during a scroll, so be wary of the code you place here.
      // We are given a few useful parameters to help us work out if we need to load some more data,
      // but first we check if we are waiting for the previous load to finish.
      @Override
      public void onScrolled(RecyclerView view, int dx, int dy) {
          int lastVisibleItemPosition = 0;
          int totalItemCount = mLayoutManager.getItemCount();
  
          if (mLayoutManager instanceof StaggeredGridLayoutManager) {
              int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
              // get maximum element within the list
              lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
          } else if (mLayoutManager instanceof GridLayoutManager) {
              lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
          } else if (mLayoutManager instanceof LinearLayoutManager) {
              lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
          }
  
          // If the total item count is zero and the previous isn't, assume the
          // list is invalidated and should be reset back to initial state
          if (totalItemCount &lt; previousTotalItemCount) {
              this.currentPage = this.startingPageIndex;
              this.previousTotalItemCount = totalItemCount;
              if (totalItemCount == 0) {
                  this.loading = true;
              }
          }
          // If it’s still loading, we check to see if the dataset count has
          // changed, if so we conclude it has finished loading and update the current page
          // number and total item count.
          if (loading &amp;&amp; (totalItemCount &gt; previousTotalItemCount)) {
              loading = false;
              previousTotalItemCount = totalItemCount;
          }
  
          // If it isn’t currently loading, we check to see if we have breached
          // the visibleThreshold and need to reload more data.
          // If we do need to reload some more data, we execute onLoadMore to fetch the data.
          // threshold should reflect how many total columns there are too
          if (!loading &amp;&amp; (lastVisibleItemPosition + visibleThreshold) &gt; totalItemCount) {
              currentPage++;
              onLoadMore(currentPage, totalItemCount, view, categoryId, searchString);
              loading = true;
          }
      }
  
      // Call this method whenever performing new searches
      public void resetState(RecyclerView view, int categoryId, String searchString) {
          this.categoryId = categoryId;
          this.searchString = searchString;
          this.currentPage = this.startingPageIndex;
          this.previousTotalItemCount = 0;
          this.loading = true;
          onScrolled(view, 0, 0);
      }
  
      // Defines the process for actually loading more data based on page
      public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view, int categoryId, String searchString);
  
  }
  </code></pre>
  
  <p>Main:
  (Important things to note are the declaration of onLoadMore() and initialization of the mEndlessScrollListener, but I included everything just in case.)</p>
  
  <pre><code>package hu.ait.macweekly;
  
  import android.content.Intent;
  import android.os.Bundle;
  import android.support.design.widget.NavigationView;
  import android.support.v4.view.GravityCompat;
  import android.support.v4.widget.DrawerLayout;
  import android.support.v4.widget.SwipeRefreshLayout;
  import android.support.v7.app.ActionBarDrawerToggle;
  import android.support.v7.widget.LinearLayoutManager;
  import android.support.v7.widget.RecyclerView;
  import android.support.v7.widget.Toolbar;
  import android.util.Log;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.View;
  import android.widget.Button;
  import android.widget.LinearLayout;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import butterknife.BindView;
  import butterknife.ButterKnife;
  import hu.ait.macweekly.adapter.ArticleRecyclerAdapter;
  import hu.ait.macweekly.data.Article;
  import hu.ait.macweekly.data.GuestAuthor;
  import hu.ait.macweekly.listeners.ArticleViewClickListener;
  import hu.ait.macweekly.listeners.EndlessRecyclerViewScrollListener;
  import hu.ait.macweekly.network.NewsAPI;
  import retrofit2.Call;
  import retrofit2.Callback;
  import retrofit2.Response;
  import retrofit2.Retrofit;
  import retrofit2.converter.gson.GsonConverterFactory;
  
  public class MainActivity extends BaseActivity
          implements NavigationView.OnNavigationItemSelectedListener,
          ArticleViewClickListener {
  
      boolean showingNewsFeed = false;
  
      // Constants
      private final String LOG_TAG = "MainActivity - ";
  
      private final int ARTICLES_PER_CALL = 25;
  
      // Members
      private NewsAPI newsAPI;
      private ArticleRecyclerAdapter mArticleAdapter;
      private EndlessRecyclerViewScrollListener mEndlessScrollListener;
  
      // Views
      @BindView(R.id.main_content) RecyclerView mMainContent;
      @BindView(R.id.refresh_view) SwipeRefreshLayout mSwipeRefreshLayout;
      @BindView(R.id.newsFeedErrorView) LinearLayout mErrorView;
      @BindView(R.id.errorButton) Button mButtonView;
  
      // Code
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          initContentViews();
  
          Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
          setSupportActionBar(toolbar);
  
          prepareDrawer(toolbar);
  
          prepareNavView();
  
          prepareNewsAPI();
  
          prepareContentViews();
  
      }
  
      private void prepareContentViews() {
          mArticleAdapter = new ArticleRecyclerAdapter(getApplicationContext(), this);
          mArticleAdapter.setDataSet(new ArrayList&lt;Article&gt;());
          LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
          mMainContent.setLayoutManager(linearLayoutManager);
          mMainContent.setAdapter(mArticleAdapter);
          mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
              @Override
              public void onRefresh() {
  //                callNewsAPI();
                  resetArticlesClear();
              }
          });
          mSwipeRefreshLayout.post(new Runnable() { // TODO: 10/29/17 Need this?
              @Override
              public void run() {
                  mSwipeRefreshLayout.setRefreshing(true);
              }
          });
  
          mEndlessScrollListener = new EndlessRecyclerViewScrollListener(linearLayoutManager) {
              @Override
              public void onLoadMore(int page, int totalItemsCount, RecyclerView view, int categoryId, String searchString) {
                  addArticles(page, categoryId, searchString);
              }
          };
          mMainContent.addOnScrollListener(mEndlessScrollListener);
  
          mButtonView.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  resetArticlesClear();
              }
          });
      }
  
      private void initContentViews() {
          setContentView(R.layout.activity_main);
          ButterKnife.bind(this);
      }
  
      private void prepareNavView() {
          NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
          navigationView.setNavigationItemSelectedListener(this);
      }
  
      private void prepareDrawer(Toolbar toolbar) {
          DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
          ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                  this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
          drawer.setDrawerListener(toggle);
          toggle.syncState();
  //        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); // TODO: 10/30/17 Turn this back on when feature finished
      }
  
      public void prepareNewsAPI() {
          Retrofit retrofit = new Retrofit.Builder()
                  .baseUrl("http://themacweekly.com")
                  .addConverterFactory(GsonConverterFactory.create())
                  .build();
          newsAPI = retrofit.create(NewsAPI.class);
      }
  
      @Override
      public void onBackPressed() {
          DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
          if (drawer.isDrawerOpen(GravityCompat.START)) {
              drawer.closeDrawer(GravityCompat.START);
          } else {
              super.onBackPressed();
          }
      }
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          // Inflate the menu; this adds items to the action bar if it is present.
          getMenuInflater().inflate(R.menu.main, menu);
          return true;
      }
  
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          // Handle action bar item clicks here. The action bar will
          // automatically handle clicks on the Home/Up button, so long
          // as you specify a parent activity in AndroidManifest.xml.
          int id = item.getItemId();
  
          //noinspection SimplifiableIfStatement
          if (id == R.id.action_refresh) {
              resetArticlesWithSearch("Aarohi");
              return true;
          }else if (id == R.id.about_page) {
              goToAboutPage();
              return true;
          }
  
          return super.onOptionsItemSelected(item);
      }
  
      private void goToAboutPage() {
          Intent aboutPageIntent = new Intent(this, AboutPage.class);
          startActivity(aboutPageIntent);
      }
  
      @SuppressWarnings("StatementWithEmptyBody")
      @Override
      public boolean onNavigationItemSelected(MenuItem item) {
          // Handle navigation view item clicks here.
          int id = item.getItemId();
  
          if (id == R.id.nav_camera) {
              // Handle the camera action
              resetArticlesWithCategory(4);
          } else if (id == R.id.nav_gallery) {
  
          } else if (id == R.id.nav_slideshow) {
  
          } else if (id == R.id.nav_manage) {
  
          } else if (id == R.id.nav_send) {
  
          }
  
          DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
          drawer.closeDrawer(GravityCompat.START);
          return true;
      }
  
      public void showNewsFeed() {
          mErrorView.setVisibility(View.GONE);
          mMainContent.setVisibility(View.VISIBLE);
          showingNewsFeed = true;
      }
      public void showErrorScreen() {
          mMainContent.setVisibility(View.GONE);
          mErrorView.setVisibility(View.VISIBLE);
          showingNewsFeed = false;
      }
  
      public interface ArticleCallback {
          void onSuccess(List&lt;Article&gt; articles);
          void onFailure();
      }
  
      private void callNewsAPI(final int pageNum, int categoryId, String searchStr, final ArticleCallback articleCallback) {
          final Call&lt;List&lt;Article&gt;&gt; articleCall;
          if(categoryId != EndlessRecyclerViewScrollListener.NO_CATEGORY // Here we build our articleCall based on what information is passed to us
                  &amp;&amp; !searchStr.equals(EndlessRecyclerViewScrollListener.NO_SEARCH)) { // If we have category or search string, use those...
              articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, categoryId, searchStr);
  
          } else if(categoryId != EndlessRecyclerViewScrollListener.NO_CATEGORY) {
              articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, categoryId);
  
          } else if(!searchStr.equals(EndlessRecyclerViewScrollListener.NO_SEARCH)) {
              articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, searchStr);
  
          } else {
              articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL);
  
          }
          Log.d(LOG_TAG, "Sent article api call ----------------");
          articleCall.enqueue(new Callback&lt;List&lt;Article&gt;&gt;() {
              @Override
              public void onResponse(Call&lt;List&lt;Article&gt;&gt; call, Response&lt;List&lt;Article&gt;&gt; response) {
  
                  mSwipeRefreshLayout.setRefreshing(false);
  
                  if (response.body() != null) {
                      Log.d(LOG_TAG, "Got response back. Page: "+pageNum+" -----------------");
  
                      List&lt;Article&gt; uncleanedResponse = response.body();
                      List&lt;Article&gt; cleanedResponse = cleanResponse(uncleanedResponse);
  
                      if(!showingNewsFeed) showNewsFeed();
                      articleCallback.onSuccess(cleanedResponse);
  
                  } else {
                      Log.e(LOG_TAG, "api response body is null. Page: "+pageNum);
  
                      articleCallback.onFailure();
                  }
              }
  
              @Override
              public void onFailure(Call&lt;List&lt;Article&gt;&gt; call, Throwable t) {
                  Log.e(LOG_TAG, "call failed. Could not retrieve page. Page: "+pageNum);
                  mSwipeRefreshLayout.setRefreshing(false);
                  articleCallback.onFailure();
              }
          });
      }
  
      private void resetArticlesClear() {
          resetArticles(EndlessRecyclerViewScrollListener.NO_CATEGORY, EndlessRecyclerViewScrollListener.NO_SEARCH);
      }
  
      private void resetArticlesWithCategory(int categoryId) {
          resetArticles(categoryId, EndlessRecyclerViewScrollListener.NO_SEARCH);
      }
  
      private void resetArticlesWithSearch(String searchString) {
          resetArticles(EndlessRecyclerViewScrollListener.NO_CATEGORY, searchString);
      }
  
      private void resetArticlesWithCatAndSearch(int categoryId, String searchString) {
          resetArticles(categoryId, searchString);
      }
  
      private void resetArticles(int categoryId, String searchString) {
          mArticleAdapter.clearDataSet();
          mArticleAdapter.notifyDataSetChanged();
          showNewsFeed();
          mEndlessScrollListener.resetState(mMainContent, categoryId, searchString);
      }
  
      private void addArticles(int pageNum, int categoryId, String searchString) {
          final int startSize = mArticleAdapter.getItemCount();
          ArticleCallback articleCallback = new ArticleCallback() {
              @Override
              public void onSuccess(List&lt;Article&gt; articles) {
                  if (!showingNewsFeed) showNewsFeed();
                  mArticleAdapter.addToDataSet(articles);
                  mArticleAdapter.notifyItemRangeChanged(startSize, ARTICLES_PER_CALL);
              }
  
              @Override
              public void onFailure() {
                  if (mArticleAdapter.getDataSet().size() == 0) showErrorScreen();
              }
          };
  
          callNewsAPI(pageNum, categoryId, searchString, articleCallback);
      }
  
      private List&lt;Article&gt; cleanResponse(List&lt;Article&gt; uncleanedResponse) {
          int MIN_CHAR_COUNT_FOR_ARTICLE = 1200; // Articles with char count &lt; this val likely only have a video or audio link which our app doesn't handle.
          //TODO: This also means however that we aren't loading things like comics or single images.
          //Ultimately we want to be able to load videos or audio.
  
  
          for (int i = uncleanedResponse.size() - 1; i &gt;= 0; i--) {
              Article article = uncleanedResponse.get(i);
  
              if (MacWeeklyUtils.isTextEmpty(article.excerpt.rendered) || article.content.rendered.length() &lt; MIN_CHAR_COUNT_FOR_ARTICLE) {
  
                  uncleanedResponse.remove(i);
              }
          }
          return uncleanedResponse;
      }
  
      @Override
      public void articleViewClicked(View view, int position) {
          showFullArticle(mArticleAdapter.getDataSet().get(position));
      }
  
      private void showFullArticle(Article targetArticle) {
  
          // These attributes might be null or missing
          String authorBio = "";
          String authorName = "";
          String authorImgUrl = "";
          if(targetArticle.guestAuthor != null) {
  
              GuestAuthor gAuthor = targetArticle.guestAuthor;
  
              if(gAuthor.name != null) {
                  authorName = targetArticle.guestAuthor.name;
              }
  
              if(!MacWeeklyUtils.isTextEmpty(gAuthor.imgUrl)) {
                  authorImgUrl = gAuthor.imgUrl;
              }
  
              if(!MacWeeklyUtils.isTextEmpty(gAuthor.bio)){
                  authorBio = gAuthor.bio;
              }
          }
  
  
          Intent articleIntent = new Intent(this, ArticleActivity.class);
          articleIntent.putExtra(ArticleActivity.ARTICLE_AUTHOR_KEY, "Author name here");
          articleIntent.putExtra(ArticleActivity.ARTICLE_CONTENT_KEY, targetArticle.content
                  .rendered);
          articleIntent.putExtra(ArticleActivity.ARTICLE_DATE_KEY, targetArticle.date);
          articleIntent.putExtra(ArticleActivity.ARTICLE_TITLE_KEY, targetArticle.title.rendered);
          articleIntent.putExtra(ArticleActivity.ARTICLE_AUTHOR_KEY, authorName);
          articleIntent.putExtra(ArticleActivity.ARTICLE_LINK_KEY, targetArticle.link);
          articleIntent.putExtra(ArticleActivity.AUTHOR_IMG_URL_KEY, authorImgUrl);
          articleIntent.putExtra(ArticleActivity.AUTHOR_BIO_KEY, authorBio);
          startActivity(articleIntent);
      }
  }
  </code></pre>
  
  <p>Additionally, here are two pictures that show the issue. In the first photo, the most recent article is March 09, which is accurate but in the second photo the most recent article is March 02, which is not correct. This article had been draw from the second page (and I assume the first page was just skipped. In the third photo we see that the March 02 article does indeed appear a bit further down the "true" list, presumably on what is the second page.</p>
  
  <p><a href="https://i.stack.imgur.com/PxJgR.png" rel="nofollow noreferrer">Infinite scroll working correctly, grabs most recent page</a></p>
  
  <p><a href="https://i.stack.imgur.com/OyII4.png" rel="nofollow noreferrer">Infinite scroll working incorrectly, starts with page 2 in pagination</a></p>
  
  <p><a href="https://i.stack.imgur.com/2MyvL.png" rel="nofollow noreferrer">Infinite scroll working correctly, showing the second page starting after the first</a></p>
  
  <p>Sorry if this is too much info, wasn't sure how much to include. Any tips in the right direction would be very much appreciated!</p>