Android Infinite List with Load More Example

Objectives Outline for This Tutorial:

  1. We will be building a news feed, which will simulate JSON data loading from the internet and then populate the list items.
  2. Till the network data is being fetched, the list will show a loading view at the bottom.
  3. We will be loading the images from urls and setting it in the view display. For this purpose we will use a library Glide.
  4. The JSON data will be seeded in the application and this seed JSON file will be stored in the assets folder.
  5. The seed file will be parsed into InfiniteFeedInfo object using another library gson.
  6. This structure will also be compatible if we are pulling url json data from a live server.

This Tutorial is based on PlaceHolderView android library. This library has InfinitePlaceHolderView class which facilitates such implementations.

Infinite PlaceHolderView:

This class is build upon the PlaceHolderView . It defines the load more callback and the loading view. The detail about this class can be found here

The best approach to understand and appreciate its usage would be to go through the process described below.

Let’s start building:

Step 1:

Setup the project in android studio with default activity.

In app’s build.gradle add the dependencies.

android {
    ...
    sourceSets {
        main {
            assets.srcDirs = ['src/main/assets', 'src/main/assets/']
            res.srcDirs = ['src/main/res', 'src/main/res/drawable']
        }
    }
}

dependencies {
    ...
    compile 'com.mindorks:placeholderview:0.7.1'
    compile 'com.android.support:cardview-v7:25.3.1'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.google.code.gson:gson:2.7'
}

Notes:

  1. Add an assets folder in the src/main directory and point to it in gradle assets.srcDirs
  2. CardView is used to display the images in the list

Add Internet permission in the app’s AndroidManifest.xml

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

Step 2:

Create src/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.mindorks.placeholderview.InfinitePlaceHolderView
        android:id="@+id/loadMoreView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

Step 3:

Create src/layout/load_more_item_view.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="4dp"
    app:cardElevation="1dp"
    android:layout_margin="5dp"
    app:cardBackgroundColor="@android:color/holo_blue_dark">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="10dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        android:paddingBottom="20dp"
        android:gravity="left">
        <android.support.v7.widget.CardView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            app:cardCornerRadius="4dp"
            app:cardElevation="1dp">
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="60dp"
            android:layout_height="80dp"
            android:layout_gravity="top"
            android:scaleType="centerCrop"/>
        </android.support.v7.widget.CardView>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:orientation="vertical">
            <TextView
                android:id="@+id/titleTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@android:color/white"
                android:textSize="16sp"
                android:typeface="sans"
                android:textStyle="normal"/>
            <TextView
                android:id="@+id/captionTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:textColor="@android:color/white"
                android:textSize="12sp"
                android:typeface="sans"
                android:textStyle="normal"/>
            <TextView
                android:id="@+id/timeTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:layout_gravity="right"
                android:textColor="@android:color/white"
                android:textSize="12sp"
                android:typeface="sans"
                android:textStyle="normal"/>
        </LinearLayout>
    </LinearLayout>
</android.support.v7.widget.CardView>

Note :

This xml defines the list item card view.

Step 4:

Create src/layout/load_more_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:padding="5dp">
    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

Note:

Progress shows the loading when the data is fetched from the server.

Step 5:

Place infinite_news.json file in the assets folder created in the above step 1.

[
	{
		"title" : "Nashville Season 5 Premiere Set at CMT",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMjA2NTE0NzkyMF5BMl5BanBnXkFtZTgwMjAwMzg5NjE@._V1._SY140_.jpg",
		"caption" : "The cancelled ABC country-music drama will make its CMT debut with a two-hour premiere on Thursday, January 5, at 9/8c, the cable network announced Wednesday",
		"time" : "1 hours ago"
	},
	{
		"title" : "Sarah Paulson Joins Ryan Murphy's FX Drama Feud as Geraldine Page",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTUzMTA3NjM4MV5BMl5BanBnXkFtZTcwNjk1NTAyMg@@._V1._SY140_.jpg",
		"caption" : "Ryan Murphy is taking a page from his successful playbook, casting frequent collaborator Sarah Paulson in his new FX anthology drama Feud.",
		"time" : "2 hours ago"
	},
	{
		"title" : "Ronald Reagan Biopic Draws ‘Soul Surfer’ Director Sean McNamara",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMzEzOTk4OTQ2OF5BMl5BanBnXkFtZTYwMzkyODQ2._V1._SY140_.jpg",
		"caption" : "\"Soul Surfer\" Sean McNamara has signed to helm a Ronald Reagan biopic that’s set to start production next spring.",
		"time" : "3 hours ago"
	},
	{
		"title" : "Martin Lawrence Gets First Stand-Up Special in 14 Years at Showtime",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTczOTMwOTc1OF5BMl5BanBnXkFtZTgwNTc3MjY5NzE@._V1._SY140_.jpg",
		"caption" : "Martin Lawrence‘s first stand-up special in 14 years will air on Showtime next month, the network announced on Tuesday.",
		"time" : "10 hours ago"
	},
	{
		"title" : "Cmt Announces ‘Nashville’ Season 5 Premiere Date",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMjgwMzg2ODgtZTkzYi00YTU0LThhYjMtMWM0Zjc0ZGFhMDViXkEyXkFqcGdeQXVyNjQxMDY5MjM@._V1._SY140_.jpg",
		"caption" : "\"Nashville\" is heading back to TV with a new network, new showrunners and a new year",
		"time" : "13 hours ago"
	},
	{
		"title" : "AFI Cancels Birth of a Nation Screening, Nate Parker Q&A",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMzk3MjE5NjE4NF5BMl5BanBnXkFtZTcwOTUzODY0OQ@@._V1._SY140_.jpg",
		"caption" : "The American Film Institute has canceled its Friday screening of “Birth of a Nation,” which was to be followed by a Q&A with filmmaker and star Nate Parker, whose 1999 rape case has put the filmmaker and distributor Fox Searchlight on the defensive this past week.",
		"time" : "2 hours ago"
	},
	{
		"title" : "Horror Movie ‘Don’t Breathe’ to Scare Off ‘Suicide Squad’ for No 1 Spot",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BZGI5ZTU2M2YtZWY4MC00ZDFhLTliYTItZTk1NjdlN2NkMzg2XkEyXkFqcGdeQXVyMjY5ODI4NDk@._V1._SY140_.jpg",
		"caption" : "After three straight weekends at No. 1, Warner Bros. comic book hit “Suicide Squad” is finally going down.",
		"time" : "4 hours ago"
	},
	{
		"title" : "After The Infiltrator Fizzles, Broad Green Plots Its Comeback",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTEwNzM2NjY2MTNeQTJeQWpwZ15BbWU4MDQ3MDI3Njgx._V1._SY140_.jpg",
		"caption" : "Gabriel Hammond was frustrated.",
		"time" : "3 hours ago"
	},
	{
		"title" : "The Fall: Series 3 Teaser Trailer: Gillian Anderson is Haunted By Killer Jamie Dornan",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTQzMTk4MjY3NF5BMl5BanBnXkFtZTcwMzI1ODAxOA@@._V1._SY140_.jpg",
		"caption" : "It’s been two years since audiences last saw Gillian Anderson as the driven and obsessive Stella Gibson, the detective superintendent who’s been after serial killer Paul Spector (played by Jamie Dornan) in BBC Two’s drama \"The Fall.\"",
		"time" : "6 hours ago"
	},
	{
		"title" : "Nashville Season 5 Premiere Set at CMT",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMjA2NTE0NzkyMF5BMl5BanBnXkFtZTgwMjAwMzg5NjE@._V1._SY140_.jpg",
		"caption" : "Nashville‘s comeback tour will kick off just after the new year.",
		"time" : "40 minutes ago"
	},
	{
		"title" : "Lifetime Developing Robert Durst TV Movie",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BNTMyMzc4NTk2NV5BMl5BanBnXkFtZTgwNjMyMzYyNDE@._V1._SY140_.jpg",
		"caption" : "The Robert Durst story is headed to TV — again.",
		"time" : "2 hours ago"
	},
	{
		"title" : "Ming-Na Wen goes from S.H.I.E.L.D. agent to time travel agent for Disney XD",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTIyNjUwNTk3OV5BMl5BanBnXkFtZTcwOTE4NTMzMQ@@._V1._SY140_.jpg",
		"caption" : "AnimationFix: Your regular round-up of the latest animation news, from HitFix reporter Emily Rome Ming-Na Wen is ever-strengthening her family ties to Disney",
		"time" : "23 hours ago"
	},
	{
		"title" : "Scott Eastwood’s Deceased Girlfriend Identified as Aspiring Model Jewel Brangman",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTczODYzOTA0Nl5BMl5BanBnXkFtZTgwNDcwMzIyNTE@._V1._SY140_.jpg",
		"caption" : "Scott Eastwood did not reveal the name of his former girlfriend in his emotional revelation about her death this week, but she has since been identified as aspiring model Jewel Brangman.",
		"time" : "10 hours ago"
	},
	{
		"title" : "Mady and Cara Gosselin Open Up About Their Estranged Relationship with Dad Jon: 'He Doesn't Even Know Us'",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMTQ1NTU3MjE1OF5BMl5BanBnXkFtZTcwODEzMjYyMw@@._V1._SY140_.jpg",
		"caption" : "Kate Gosselin and her kids open up about how finding reality TV fame more than 10 years ago changed their lives forever. Subscribe now for an inside look at how they've overcome setbacks and remain hopeful for the future",
		"time" : "12 hours ago"
	},
	{
		"title" : "Dream Getaways: Celebrity Couples' Luxurious Vacation Homes",
		"image_url" : "http://ia.media-imdb.com/images/M/MV5BMjEyMTEyOTQ0MV5BMl5BanBnXkFtZTcwNzU3NTMzNw@@._V1._SY140_.jpg",
		"caption" : "Who needs one house when you can have two?!  Celebrity couples already live a life of luxury in their everyday mansions, but some husband and wives opt to purchase another home for when they feel like getting away on vacation.",
		"time" : "23 hours ago"
	}
]

Notes:

  1. This strategy is very useful in bundling app with seed files. Seed files contain data build in the app package and can be used to populate database or use to display default data to the user. Placing seed files in the form of json makes is extremely easy to parse into models.

Step 6:

Create Utils.java

public class Utils {

    private static final String TAG = "Utils";

    public static List<InfiniteFeedInfo> loadInfiniteFeeds(Context context){
        try{
            GsonBuilder builder = new GsonBuilder();
            Gson gson = builder.create();
            JSONArray array = new JSONArray(loadJSONFromAsset(context, "infinite_news.json"));
            List<InfiniteFeedInfo> feedList = new ArrayList<>();
            for(int i=0;i<array.length();i++){
                InfiniteFeedInfo feed = gson.fromJson(array.getString(i), InfiniteFeedInfo.class);
                feedList.add(feed);
            }
            return feedList;
        }catch (Exception e){
            Log.d(TAG,"seedGames parseException " + e);
            e.printStackTrace();
            return null;
        }
    }

    private static String loadJSONFromAsset(Context context, String jsonFileName) {
        String json = null;
        InputStream is=null;
        try {
            AssetManager manager = context.getAssets();
            Log.d(TAG,"path "+jsonFileName);
            is = manager.open(jsonFileName);
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            json = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
        return json;
    }
}

Note:

  1. Utils contain methods required to parse seed json file and also populate the model InfiniteFeedInfo.java

Step 7:

Create model InfiniteFeedInfo

public class InfiniteFeedInfo {

    @SerializedName("title")
    @Expose
    private String title;

    @SerializedName("image_url")
    @Expose
    private String imageUrl;

    @SerializedName("caption")
    @Expose
    private String caption;

    @SerializedName("time")
    @Expose
    private String time;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImageUrl() {
        return imageUrl;
    }

    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    public String getCaption() {
        return caption;
    }

    public void setCaption(String caption) {
        this.caption = caption;
    }

    public String getTime() {
        return time;
    }

    public void setTime(String time) {
        this.time = time;
    }
}

Notes:

  1. @SerializedName annotation belongs to gson class and used to read json file variable and bind it to the model variable.
  2. @Exposeis used to make the variable readable for the gson parsing

Step 8:

We will now create the class to bind the item views of the list and its operations.Create ItemView.java for the item view of the list.

@Layout(R.layout.load_more_item_view)
public class ItemView {

    @View(R.id.titleTxt)
    private TextView titleTxt;

    @View(R.id.captionTxt)
    private TextView captionTxt;

    @View(R.id.timeTxt)
    private TextView timeTxt;

    @View(R.id.imageView)
    private ImageView imageView;

    private InfiniteFeedInfo mInfo;
    private Context mContext;

    public ItemView(Context context, InfiniteFeedInfo info) {
        mContext = context;
        mInfo = info;
    }

    @Resolve
    private void onResolved() {
        titleTxt.setText(mInfo.getTitle());
        captionTxt.setText(mInfo.getCaption());
        timeTxt.setText(mInfo.getTime());
        Glide.with(mContext).load(mInfo.getImageUrl()).into(imageView);
    }
}

Notes:

  1. @layout is used to bind the xml layout with this class.
  2. @View is used to bind the views in a layout we want to refer to.
  3. @Resolvecalls a method when the view is in the memory so to populate it with the data.

For detailed explanations view PlaceHolderView at GitHub repository

Step 9:

We will now create the class to bind and define the load more view.

Create class LoadMoreView.java

@Layout(R.layout.load_more_view)
public class LoadMoreView {

    public static final int LOAD_VIEW_SET_COUNT = 6;

    private InfinitePlaceHolderView mLoadMoreView;
    private List<InfiniteFeedInfo> mFeedList;

    public LoadMoreView(InfinitePlaceHolderView loadMoreView, List<InfiniteFeedInfo> feedList) {
        this.mLoadMoreView = loadMoreView;
        this.mFeedList = feedList;
    }

    @LoadMore
    private void onLoadMore(){
        Log.d("DEBUG", "onLoadMore");
        new ForcedWaitedLoading();
    }

    class ForcedWaitedLoading implements Runnable{

        public ForcedWaitedLoading() {
            new Thread(this).start();
        }

        @Override
        public void run() {

            try {
                Thread.currentThread().sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    int count = mLoadMoreView.getViewCount();
                    Log.d("DEBUG", "count " + count);
                    for (int i = count - 1;
                         i < (count - 1 + LoadMoreView.LOAD_VIEW_SET_COUNT) && mFeedList.size() > i;
                         i++) {
                        mLoadMoreView.addView(new ItemView(mLoadMoreView.getContext(), mFeedList.get(i)));

                        if(i == mFeedList.size() - 1){
                            mLoadMoreView.noMoreToLoad();
                            break;
                        }
                    }
                    mLoadMoreView.loadingDone();
                }
            });
        }
    }
}

Notes:

  1. @LoadMore This annotation is called when the list is scrolled to the last item. Any network call to fetch more data should be done in this annotated method.
  2. For the simulation of delayed loading of data from the internet, I have defined ForcedWaitedLoading class. This class creates a new thread and then after 2 sec, adds more data int view list, in the UI thread. The view should always be updated from UI thread.
  3. mLoadMoreView.loadingDone() method need to be called when all the data has been fetched and view has been populated with new data. It will remove the load more view.
  4. mLoadMoreView.noMoreToLoad() method should be called when all the data has been fetched. It will remove the load more functionality.

Step 10:

Create MainActivity.java

public class MainActivity extends AppCompatActivity {

    private InfinitePlaceHolderView mLoadMoreView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mLoadMoreView = (InfinitePlaceHolderView)findViewById(R.id.loadMoreView);
        setupView();
    }

    private void setupView(){

        List<InfiniteFeedInfo> feedList = Utils.loadInfiniteFeeds(this.getApplicationContext());
        Log.d("DEBUG", "LoadMoreView.LOAD_VIEW_SET_COUNT " + LoadMoreView.LOAD_VIEW_SET_COUNT);
        for(int i = 0; i < LoadMoreView.LOAD_VIEW_SET_COUNT; i++){
            mLoadMoreView.addView(new ItemView(this.getApplicationContext(), feedList.get(i)));
        }
        mLoadMoreView.setLoadMoreResolver(new LoadMoreView(mLoadMoreView, feedList));
    }
}

Notes:

  1. We obtain the instance of the InfinitePlaceHolderView and add views using the InfiniteFeedInfo data from the model list.
  2. setLoadMoreResolver method adds the LoadMoreView object to be used as the indicator for loading.

PlaceHolderView GitHub repository is hereThe source code for this example is here

Coders Rock!!