Different approach would be to start the API call inside onBindViewHolder and initialy place into items view some progress indicator. After call is finished, you update the view (hide progress and showing received data). For example with Picasso for image loading, onBindViewHolder method would look like this
@Override
public void onBindViewHolder(final MovieViewHolder holder, final int position) {
final Movie movie = items.get(position);
holder.imageProgress.setVisibility(View.VISIBLE);
Picasso.with(context)
.load(NetworkingUtils.getMovieImageUrl(movie.getPosterPath()))
.into(holder.movieThumbImage, new Callback() {
@Override
public void onSuccess() {
holder.imageProgress.setVisibility(View.GONE);
}
@Override
public void onError() {
}
});
}
As I see it, there are two cases which can appear:
- where you download all items in light version with one call (e.g. the adapter knows immediately that he’ll have to deal with 40 pictures, but downloads it on demand —> case which I showed previously with Picasso)
- where you are working with real lazy loading and you are asking server to give you additional chunk of data. In this case, first prerequisite is to have adequate response from server with necessary information. Fore example
{
"offset": 0,
"total": 100,
"items": [{items}]
}
There response means that you received first chunk of total 100 data. My approach would be something like this:
View
After getting first chunk of data (e.g. 10) add them into adapter.
RecyclerView.Adapter.getItemCount
As long as the current amount of available items is lower than total amount (e.g. available 10; total 100), in getItemCount method you will return items.size() + 1
RecyclerView.Adapter.getItemViewType
if total amount of data is greater than amount of available items in adapter and the position = items.size() (i.e. you’ve fictively added item in getItemCount method), as view type you return some progress-indicator. Otherwise you’ll return normal layout type
RecyclerView.Adapter.onCreateViewHolder
When you are asked to use progress-indicator view type, all you need to do is to ask your presenter to get additional chunk of items and update the adapter
So basically, this is approach where you don’t have to add/remove items from the list and where you have control over situation when lazy loading will be triggered.
Here is the code example:
public class ForecastListAdapter extends RecyclerView.Adapter<ForecastListAdapter.ForecastVH> {
private final Context context;
private List<Forecast> items;
private ILazyLoading lazyLoadingListener;
public static final int VIEW_TYPE_FIRST = 0;
public static final int VIEW_TYPE_REST = 1;
public static final int VIEW_TYPE_PROGRESS = 2;
public static final int totalItemsCount = 14;
public ForecastListAdapter(List<Forecast> items, Context context, ILazyLoading lazyLoadingListener) {
this.items = items;
this.context = context;
this.lazyLoadingListener = lazyLoadingListener;
}
public void addItems(List<Forecast> additionalItems){
this.items.addAll(additionalItems);
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if(totalItemsCount > items.size() && position == items.size()){
return VIEW_TYPE_PROGRESS;
}
switch (position){
case VIEW_TYPE_FIRST:
return VIEW_TYPE_FIRST;
default:
return VIEW_TYPE_REST;
}
}
@Override
public ForecastVH onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
switch (viewType){
case VIEW_TYPE_PROGRESS:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_progress, parent, false);
if (lazyLoadingListener != null) {
lazyLoadingListener.getAdditionalItems();
}
break;
case VIEW_TYPE_FIRST:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_first, parent, false);
break;
default:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_rest, parent, false);
break;
}
return new ForecastVH(v);
}
@Override
public void onBindViewHolder(ForecastVH holder, int position) {
if(position < items.size()){
Forecast item = items.get(position);
holder.date.setText(FormattingUtils.formatTimeStamp(item.getDt()));
holder.minTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMin()));
holder.maxTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMax()));
}
}
@Override
public long getItemId(int position) {
long i = super.getItemId(position);
return i;
}
@Override
public int getItemCount() {
if (items == null) {
return 0;
}
if(items.size() < totalItemsCount){
return items.size() + 1;
}else{
return items.size();
}
}
public class ForecastVH extends RecyclerView.ViewHolder{
@BindView(R.id.forecast_date)TextView date;
@BindView(R.id.min_temperature)TextView minTemperature;
@BindView(R.id.max_temperature) TextView maxTemperature;
public ForecastVH(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
public interface ILazyLoading{
public void getAdditionalItems();
}}
Maybe this'll inspire you to make something that will suit your needs
notifyItemInserted()
andnotifyItemRemoved()
while the user is scrolling leads to some lagging and jarring effects in the list. This always happens with this approach, especially when the user flings the list. Google's own apps show the spinner in their paginated lists too, but without any kind of lagging or shifting of the list items. I conclude that they have used some other approach that does not involve adding anull
object into the data set midway while the user is scrolling ... so I am interested in their solution ... :) – Embower