Android Infinite List with Load More Example
![Android Infinite List with Load More Example](https://mindorks.files.wordpress.com/2018/01/2f28b-18mjs6tuqy0waffi8hbe-yq.gif)
Objectives Outline for This Tutorial:
- We will be building a news feed, which will simulate JSON data loading from the internet and then populate the list items.
- Till the network data is being fetched, the list will show a loading view at the bottom.
- We will be loading the images from urls and setting it in the view display. For this purpose we will use a library Glide.
- The JSON data will be seeded in the application and this seed JSON file will be stored in the assets folder.
- The seed file will be parsed into
InfiniteFeedInfo
object using another library gson. - 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:
- Add an
assets
folder in the src/main directory and point to it in gradle assets.srcDirs 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:
- 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:
Utils
contain methods required to parse seed json file and also populate the modelInfiniteFeedInfo.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:
@SerializedName
annotation belongs to gson class and used to read json file variable and bind it to the model variable.@Expose
is 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:
@layout
is used to bind the xml layout with this class.@View
is used to bind the views in a layout we want to refer to.@Resolve
calls 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:
@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.- 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. 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.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:
- We obtain the instance of the
InfinitePlaceHolderView
and add views using theInfiniteFeedInfo
data from the model list. setLoadMoreResolver
method adds theLoadMoreView
object to be used as the indicator for loading.
PlaceHolderView GitHub repository is hereThe source code for this example is here