Android Endless Scrolling RecyclerView

Today, our walkthrough will be on how to implement endless scrolling RecyclerView. I have been doing research on this topic until I achieved a smart yet simple way on how to implement this behavior. Generally, you might have found some apps like Reddit, Facebook and Gmail apps implementing this behavior, the same thing they do, is the same thing we are going to achieve under this tutorial

Introduction to endless scrolling RecyclerView

Achieving endless scrolling ReccyelrView is quite easy. All we need to do is do understand when the user is scrolling down and try to understand when the user reaches the bottom of the RecyclerView. Please have a look at the code below as it explains how to detect when the user is scrolling downwards or upwards of the recycler view.

RecyclerView rv = (RecyclerView)findViewById(R.id.rv);

rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

if (dy < 0) {
// Recycle view scrolling up...

} else if (dy > 0) {
// Recycle view scrolling down...
}
}
});

This code introduces us to the magic of the day. We will ignore onScrollStateChanged(RecyclerView recyclerView, int newState) method, as we don’t need it here. We will also ignore when the user is scrolling upwards and concentrate only when the user is scrolling downwards. You might have noticed something, our code above only tells us when the user is scrolling downwards but it does not tell us when a user reaches the bottom of recyclerView. How do we do that? Fortunately, RecyclerView has some build-in methods and fields that will help us detect every time the user reaches the bottom. Please look at the code below:


RecyclerView rv = (RecyclerView)findViewById(R.id.rv);

rv.addOnScrollListener(new RecyclerView.OnScrollListener() {

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

if (dy > 0) {
// Recycle view scrolling down...
if(recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN) == false){
Toast.makeText(getApplicationContext(), "Reached the end of recycler view", Toast.LENGTH_LONG).show();
}

}
}
});

!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN) makes us detect when the user has reached the bottom of the RecyclerView. Remember we can proudly replace false with ! without having to change any meaning to the statement

You may also like:


Endless Scrolling RecyclerView Concept and Coding

Now we may start our main business of the day. To load more we will be loading data from a PHP page. Our JSON feed is available here https://hacksmile.com/hack_smile_tutorials/loadmore.php?limit=15. Please note to understand about limit, you must first study java and PHP code. Feel free to test with this JSON feed, it contains 250 objects which is more than enough for this tutorial. Please watch the following video to get the full concept.

Android Project

We will start with android development and then go to php. Please study code carefully. Now create new android project and name it almost anything you want. I will call mine New Application. We just need one activity. Having done that, please add volley gradle dependencies. For this tutorial I will not be using volley singleton so as to make sure no one is left behind.

compile 'com.android.volley:volley:1.0.0'

Permissions
Please add internet permissions to your android manifest file.

<uses-permission android:name="android.permission.INTERNET" />

content_main.xml
This is layout of project’s MainActivity.java

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:wheel="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="3dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="hacksmile.com.MainActivity"
tools:showIn="@layout/activity_main">

<android.support.v7.widget.RecyclerView
android:id="@+id/loadmore_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/progress_wheel"
android:scrollbars="vertical" />

<!-- We need this loading wheel, only when loading more -->
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/progress_wheel"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="gone"
wheel:matProg_barColor="@color/colorPrimary"
wheel:matProg_progressIndeterminate="true" />

</RelativeLayout>

custom_row_layout.xml
This is RecyclerView’s custom row. Each row will be represented by this layout.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="1dp"
android:background="#fff"
android:orientation="vertical">

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginBottom="10dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:text="@string/test_title"
android:textColor="@color/colorPrimary"
android:textSize="17sp"
android:textStyle="bold" />

<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:text="@string/test_description"
android:textColor="#000" />

</RelativeLayout>

MainActivity.java

package hacksmile.com;

import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.Volley;
import com.pnikosis.materialishprogress.ProgressWheel;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

// we will be loading 15 items per page or per load
// you can change this to fit your specifications.
// When you change this, there will be no need to update your php page,
// as php will be ordered what to load and limit by android java
private static final int LOAD_LIMIT = 15;

// last id to be loaded from php page,
// we will need to keep track or database id field to know which id was loaded last
// and where to begin loading
private String lastId = "0"; // this will issued to php page, so no harm make it string

// we need this variable to lock and unlock loading more
// e.g we should not load more when volley is already loading,
// loading will be activated when volley completes loading
private boolean itShouldLoadMore = true;

// initialize adapter and data structure here
private RecyclerAdapter recyclerAdapter;
private ArrayList<RecyclerModel> recyclerModels;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

// you must assign all objects to avoid nullPointerException
recyclerModels = new ArrayList<>();
recyclerAdapter = new RecyclerAdapter(recyclerModels);

RecyclerView recyclerView = (RecyclerView) this.findViewById(R.id.loadmore_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);

//we can now set adapter to recyclerView;
recyclerView.setAdapter(recyclerAdapter);

// create a function for the first load
firstLoadData();

// here add a recyclerView listener, to listen to scrolling,
// we don't care when user scrolls upwards, will only be careful when user scrolls downwards
// this listener is freely provided for by android, no external library
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

// for this tutorial, this is the ONLY method that we need, ignore the rest
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
// Recycle view scrolling downwards...
// this if statement detects when user reaches the end of recyclerView, this is only time we should load more
if (!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN)) {
// remember "!" is the same as "== false"
// here we are now allowed to load more, but we need to be careful
// we must check if itShouldLoadMore variable is true [unlocked]
if (itShouldLoadMore) {
loadMore();
}
}

}
}
});

setSupportActionBar(toolbar);
}

// this function will load 15 items as indicated in the LOAD_LIMIT variable field
private void firstLoadData() {

String url = "https://hacksmile.com/hack_smile_tutorials/loadmore.php?limit=" + LOAD_LIMIT;
// to make you understand everything, to the php page, we will be doing something like this
// $limit = $_GET['limit']
// then [SELECT * FROM table_name ORDER_BY id DESC LIMIT $limit ]

itShouldLoadMore = false; // lock this guy,(itShouldLoadMore) to make sure,
// user will not load more when volley is processing another request
// only load more when volley is free

final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(false);
progressDialog.show();

JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(Request.Method.GET, url, null, new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
progressDialog.dismiss();
// remember here we are in the main thread, that means,
//volley has finished processing request, and we have our response.
// What else are you waiting for? update itShouldLoadMore = true;
itShouldLoadMore = true;

if (response.length() <= 0) {
// no data available
Toast.makeText(MainActivity.this, "No data available", Toast.LENGTH_SHORT).show();

return;
}

for (int i = 0; i < response.length(); i++) {
try {
JSONObject jsonObject = response.getJSONObject(i);

// please note this last id how we have updated it
// if there are 4 items for example, and we are ordering in descending order,
// then last id will be 1. This is because outside a loop, we will get the last
// value [Thanks to JAVA]

lastId = jsonObject.getString("id");
String title = jsonObject.getString("title");
String description = jsonObject.getString("description");

recyclerModels.add(new RecyclerModel(title, description));
recyclerAdapter.notifyDataSetChanged();

} catch (JSONException e) {
e.printStackTrace();
}
}

// please note how we have updated our last id variable which is initially 0 (String)
// outside the loop, java will return the last value, so here it will
// certainly give us lastId that we need

}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// also here, volley is not processing, unlock it should load more
itShouldLoadMore = true;
progressDialog.dismiss();
Toast.makeText(MainActivity.this, "network error!", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(MainActivity.this)
.setMessage(error.toString())
.show();
}
});

Volley.newRequestQueue(this).add(jsonArrayRequest);

}

private void loadMore() {

String url = "https://hacksmile.com/hack_smile_tutorials/loadmore.php?action=loadmore&lastId=" + lastId + "&limit=" + LOAD_LIMIT;
// our php page starts loading from 250 to 1, because we have [ORDER BY id DESC]
// So until you clearly understand everything, for this tutorial use ORDER BY ID DESC
// so we will do something like this to the php page
//==============================================
// $limit = $_GET['limit']
// $lastId = $_GET['lastId']
// then [SELECT * FROM table_name WHERE id < $lastId ORDER_BY id DESC LIMIT $limit ]
// here we shall load 15 items from table where lastId id less than last loaded id

// if you are using [ASC] in sql, your query might change to tis
// then [SELECT * FROM table_name WHERE id > $lastId ORDER_BY id DESC LIMIT $limit ]
// for this tutorial let's stick to [DESC]

itShouldLoadMore = false; // lock this until volley completes processing

// progressWheel is just a loading spinner, please see the content_main.xml
final ProgressWheel progressWheel = (ProgressWheel) this.findViewById(R.id.progress_wheel);
progressWheel.setVisibility(View.VISIBLE);

JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(Request.Method.GET, url, null, new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
progressWheel.setVisibility(View.GONE);

// since volley has completed and it has our response, now let's update
// itShouldLoadMore

itShouldLoadMore = true;

if (response.length() <= 0) {
// we need to check this, to make sure, our dataStructure JSonArray contains
// something
Toast.makeText(MainActivity.this, "no data available", Toast.LENGTH_SHORT).show();
return; // return will end the program at this point
}

for (int i = 0; i < response.length(); i++) {
try {
JSONObject jsonObject = response.getJSONObject(i);

// please note how we have updated the lastId variable
// if there are 4 items for example, and we are ordering in descending order,
// then last id will be 1. This is because outside a loop, we will get the last
// value

lastId = jsonObject.getString("id");
String title = jsonObject.getString("title");
String description = jsonObject.getString("description");

recyclerModels.add(new RecyclerModel(title, description));
recyclerAdapter.notifyDataSetChanged();

} catch (JSONException e) {
e.printStackTrace();
}
}

}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
progressWheel.setVisibility(View.GONE);
// volley finished and returned network error, update and unlock itShouldLoadMore
itShouldLoadMore = true;
Toast.makeText(MainActivity.this, "Failed to load more, network error", Toast.LENGTH_SHORT).show();

}
});

Volley.newRequestQueue(this).add(jsonArrayRequest);

}

}

RecyclerModel.java



This is RecyclerView Adapter data model.

package com.hacksmile.tutorials;

public class RecyclerModel {
private String title;
private String description;

public RecyclerModel(String title, String description) {
this.title = title;
this.description = description;
}

public String getTitle() {
return title;
}

public String getDescription() {
return description;
}
}

RecyclerAdapter.java
Our recyclerView Adapter

package hacksmile.com;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;

class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.MyViewHolder> {

private ArrayList<RecyclerModel> recyclerModels; // this data structure carries our title and description

public RecyclerAdapter(ArrayList<RecyclerModel> recyclerModels) {
this.recyclerModels = recyclerModels;
}

@Override
public RecyclerAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// inflate your custom row layout here
return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_row_layout, parent, false));
}

@Override
public void onBindViewHolder(RecyclerAdapter.MyViewHolder holder, int position) {
// update your data here

holder.title.setText(recyclerModels.get(position).getTitle());
holder.description.setText(recyclerModels.get(position).getDescription());

}

@Override
public int getItemCount() {
return recyclerModels.size();
}

class MyViewHolder extends RecyclerView.ViewHolder {
// view this our custom row layout, so intialize your variables here
private TextView title;
private TextView description;

MyViewHolder(View view) {
super(view);

title = (TextView) view.findViewById(R.id.title);
description = (TextView) view.findViewById(R.id.description);

}
}
}

PHP CODE

This section will look at the php server side code. All load more manipulation will take place here. If you studied java code correctly, and especially the comments inside the code, you will get this quickly.

function loadData($limit){
require "config.inc.php";

$query = $con->prepare("SELECT * FROM loadmore ORDER BY id DESC LIMIT $limit ");
$query->execute();
$array = array();

while($data = $query->fetch(PDO::FETCH_ASSOC)){
$id = $data['id'];
$title = $data['title'];
$description = $data['description'];
array_push($array, array(
"id" => $id,
"title" => $title,
"description" => $description
)
);
}

echo json_encode($array);

}

function loadMoreData($lastId, $limit){
require "config.inc.php";
try{
$query = $con->prepare("SELECT * FROM loadmore WHERE id < $lastId ORDER BY id DESC LIMIT $limit "); $query->execute();
$array = array();

while($data = $query->fetch(PDO::FETCH_ASSOC)){
$id = $data['id'];
$title = $data['title'];
$description = $data['description'];
array_push($array, array(
"id" => $id,
"title" => $title,
"description" => $description
)
);
}

echo json_encode($array);
} catch(Exception $e){
die($e->getMessage());
}
}

if(isset($_GET['action']) && $_GET['action'] == "loadmore"){
$lastId = $_GET['lastId'];
// this is teh limit set in the android java code (LOAD_LIMIT)
$limit = $_GET['limit'];
loadMoreData($lastId, $limit);
} else {
$limit = $_GET['limit'];
loaddata($limit);
}

config.inc.php

<?php

$servername = "localhost";
$username = "root";
$passwordData = "";
$dbname = "hackmsile_tutorials";

try {
$con = new PDO("mysql:host=$servername;dbname=$dbname", $username, $passwordData);
$con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(PDOException $e) {
die("Database error due to: ".$e->getMessage());
}

Conclusion



This marks the end of our tutorial. In case you have any question, you can ask me in the commenting system below. Code and apk will be shared later. Thanks