Load data from internet in RecyclerView.Adapter - android

I am getting movie data from the internet like its name, poster etc, but for the genres of the movie, I need to fetch it again from the net. So here's my solution for this problem.
public class MoviesViewAllAdapter extends RecyclerView.Adapter<MoviesViewAllAdapter.MoviesViewHolder> {
private Context mContext;
private List<MovieBrief> mMovies;
public MoviesViewAllAdapter(Context context, List<MovieBrief> movies) {
mContext = context;
mMovies = movies;
}
#Override
public MoviesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MoviesViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_movie_large,parent,false));
}
#Override
public void onBindViewHolder(MoviesViewHolder holder, int position) {
holder.movieGenreTextView.setText("");
setGenres(holder, mMovies.get(position).getId());
}
#Override
public int getItemCount() {
return mMovies.size();
}
public class MoviesViewHolder extends RecyclerView.ViewHolder {
public TextView movieGenreTextView;
public MoviesViewHolder(View itemView) {
super(itemView);
movieGenreTextView = (TextView) itemView.findViewById(R.id.text_view_genre_movie_card);
}
}
private void setGenres(final MoviesViewHolder holder, Integer movieId) {
ApiInterface apiService = ApiClient.getClient().create(ApiInterface.class);
Call<Movie> call = apiService.getMovieDetails(movieId,mContext.getResources().getString(R.string.MOVIE_DB_API_KEY));
call.enqueue(new Callback<Movie>() {
#Override
public void onResponse(Call<Movie> call, Response<Movie> response) {
if(response.code() != 200) return;
List<Genre> genresList = response.body().getGenres();
String genres = "";
for (int i=0;i<genresList.size();i++) {
if(i == genresList.size()-1) {
genres = genres.concat(genresList.get(i).getGenreName());
}
else {
genres = genres.concat(genresList.get(i).getGenreName()+", ");
}
}
holder.movieGenreTextView.setText(genres);
}
#Override
public void onFailure(Call<Movie> call, Throwable t) {
}
});
}
}
But here problem is that when performing fling and going up the recyclerview the genres which are showing is not related to the movie, the genres which are loading are random.
May be because I am loading data on onBindViewHolder and when holder disappears from screen it loads into random holder. Is it so ?

I believe your problem is that you are getting the entire movie list every time you create a view in your list. I am not sure how the server is returning that data but my guess is that every time you call the server there is no guarantee that the data is in the same order. You are getting a random order every time but trying to extract a fixed position of it and that is why the genres are not related.
The problematic line is onBindViewHolder which is called every time a view is created and this function is calling setGenres which is getting a new movie list which is in random order.
You can do two things to fix this issue:
Search for the movie first, find its index and then use that to get the genre. But this is still a very bad design since for a list of N movies, you are calling the server N times.
Get the list first, store it as an ArrayList in your Adapter. Now iterate through it without having to call the server each time
public class MoviesViewAllAdapter extends RecyclerView.Adapter<MoviesViewAllAdapter.MoviesViewHolder> {
.
.
.
List<Movie> list = new ArrayList<>;
public void setList(List movies){
//get data from server before creating the adapter. call this on your adapter and store the data here
this.list = movies;
}
private void setGenres(final MoviesViewHolder holder, Integer movieId){
//iterate the field list instead of calling the server
}
.
.
.
}

The problem with the wrong genres being displayed is caused by the recycling of the view/holder preformed by the RecyclerView . When you scroll instances of MoviesViewHolder and the view are reused so when you trigger the loading of a Movie details a ViewHolder is associated to a MovieRef but by the time the call to get the movie details ends the holder is now assigned to a different movie.
The best thing to do in my opinion is to load the Movie details and cache them in a map for example HashMap<Integer, Movie> mMoviesDetails; when the call to the API ends you can store the Movie object in there.
public void onResponse(Call<Movie> call, Response<Movie> response) {
if(response.code() != 200) return;
mMoviesDetails.put(movieId, response.body());
notifyDataSetChanged();
}
Then in your adapter you can change the onBindViewHolder to something like the below :
#Override
public void onBindViewHolder(MoviesViewHolder holder, int position) {
holder.movieGenreTextView.setText("");
Movie movie = mMoviesDetails.get(mMovies.get(position).getId());
if(movie != null){
/// set data to holder
}else {
//load data from network
loadGenres(mMovies.get(position).getId());
}
}
This is just sample code based on your current implementation, generally speaking I would not load this data inside an Adapter but delegate this type of tasks to a dedicated API class that can store the data in a database like Realm for example instead of using a map.

Related

Can an asynchronous call be made before the views are configured?

I'm trying to get JSON data to display in a RecyclerView list, but whenever I try to make the call it seems that the RecyclerView comes up empty. I have it set up so that I get the API service/manager in onCreate prior to the configViews() method which I wrote. Am I making the call too early? I thought the problem would be to create the views/adapter prior to the call, but it doesn't seem to be making a difference.
This is the code for the Retrofit call:
listCall.enqueue(new Callback<List<Character>>() {
#Override
public void onResponse(Call<List<Character>> call, Response<List<Character>> response) {
if(response.isSuccessful()){
List<Character> characterList = response.body();
Log.v(TAG, response.toString());
for (int i = 0; i < characterList.size(); i++){
Character character = characterList.get(i);
character.setName(characterList.get(i).getName());
character.setDescription(characterList.get(i).getDescription());
characterAdapter.addCharacter(character);
}
configViews();
characterAdapter.notifyDataSetChanged();
}
else {
int sc = response.code();
}
}
#Override
public void onFailure(Call<List<Character>> call, Throwable t) {
}
});
And that configViews() method is here, albeit simpler than when I first started (I have been moving bits around to test whether they will affect inflation of the RecyclerView):
private void configViews() {
characterAdapter = new CharacterAdapter(this);
recyclerView.setAdapter(characterAdapter);
}
EDIT: Thank you all for your replies! As requested here is the Adapter class:
public class CharacterAdapter extends
RecyclerView.Adapter<CharacterAdapter.Holder> {
private static final String TAG = CharacterAdapter.class.getSimpleName();
private final CharacterClickListener clickListener;
private List<Character> characters;
//Constructor for CharacterAdapter.
public CharacterAdapter(CharacterClickListener listener){
characters = new ArrayList<>();
clickListener = listener;
}
//Inflates CardView layout
#Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
View row =
LayoutInflater.from(parent.getContext()).inflate(R.layout.row_item, parent,
false);
return new Holder(row);
}
#Override
public void onBindViewHolder(Holder holder, int position) {
Character currentCharacter = characters.get(position);
holder.name.setText(currentCharacter.getName());
holder.description.setText(currentCharacter.getDescription());
//Picasso loads image from URL
Picasso.with(holder.itemView.getContext())
.load("http://gateway.marvel.com/"+
currentCharacter.getThumbnail()).into(holder.thumbnail);
}
#Override
public int getItemCount() {
return characters.size();
}
public void addCharacter(Character character) {
//Log.d(TAG, character.getThumbnail());
characters.add(character);
notifyDataSetChanged();
}
public Character getSelectedCharacter(int position) {
return characters.get(position);
}
public class Holder extends RecyclerView.ViewHolder implements
View.OnClickListener{
//Holder class created to be implemented by adapter.
private ImageView thumbnail;
private TextView name, description;
public Holder(View itemView) {
super(itemView);
thumbnail = (ImageView)
itemView.findViewById(R.id.character_thumbnail);
name = (TextView) itemView.findViewById(R.id.character_name);
description = (TextView)
itemView.findViewById(R.id.character_description);
itemView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
clickListener.onClick(getLayoutPosition());
}
}
public interface CharacterClickListener {
void onClick(int position);
}
}
In principle you have done the right thing. You can instantiate the views before and fire a fetch request. Once you receive the response, you can notify the adapter that the datatset has changed and the adapter will refresh your views.
The only correction here is, you need to call your configViews(); before the for loop. So your code should be like -
configViews();
for (int i = 0; i < characterList.size(); i++){
Character character = characterList.get(i);
character.setName(characterList.get(i).getName());
character.setDescription(characterList.get(i).getDescription());
characterAdapter.addCharacter(character);
}
characterAdapter.notifyDataSetChanged();
Because right now what is happening is you add all your data using add character and then after that you again call config views which reinitialises your adapter at
characterAdapter = new CharacterAdapter(this);
Hence the empty views.
PS: I don't know your adapter's addCharacter method but i am hoping it is doing the right job. If it still doesn't work let me know and then add your addCharacter code as well.
Based on code the snippets, in configViews() method you are creating new instance of CharacterAdapter. The following code snippets will work.
private void configViews() {
//characterAdapter = new CharacterAdapter(this);
recyclerView.setAdapter(characterAdapter);
}
You should leave the call configViews() call in onCreate but move the rest of it into onResume. It'll achieve following:
Ensure views are ready to display data
Refresh data if your app was brought to background and re-opened

RecyclerView blocking ui thread during updates

There are more than 200 items in my list. RecyclerView is updated regularly (Every 10 seconds) . RecyclerView blocking ui thread for few seconds during updates. I'm using notifyDataSetChanged method for refresh recyclerview. Is there another way to prevent freezing ? By the way I don't want use pagination.
This method run every 10 seconds :
public void refreshAssetList(List<Asset> newAssetList){
recyclerViewAdapter.setAssetList(newAssetList);
recyclerViewAdapter.notifyDataSetChanged();
}
RecyclerViewAdapter class :
public class AssetListRecyclerViewAdapter extends RecyclerView.Adapter<AssetListRecyclerViewAdapter.BaseViewHolder> {
private List<Asset> assetList;
Context context;
public AssetListRecyclerViewAdapter(List<Asset> assetList, Context context) {
this.assetList = assetList;
this.context = context;
}
public void setAssetList(List<Asset> assetList) {
this.assetList = assetList;
}
#Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemLayoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_asset, null);
return new DetailViewHolder(itemLayoutView);
}
#Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
Asset asset = assetList.get(position);
Last last = asset.getLast();
if (holder.getItemViewType() == TYPE_DETAIL) {
DetailViewHolder mHolder = (DetailViewHolder) holder;
mHolder.dateTextView.setText(last.getDate());
mHolder.brandTextView.setText(asset.getMc());
}
}
class DetailViewHolder extends BaseViewHolder {
#Bind(R.id.brandTextV)
TextView brandTextView;
#Bind(R.id.dateTextV)
TextView dateTextView;
DetailViewHolder(View itemLayoutView) {
super(itemLayoutView);
ButterKnife.bind(this, itemLayoutView);
}
}
}
You do not need to call notifyDataSetChanged, it's an expensive operation your whole RecyclerView will completely redraw, rebind etc.
As the doc says:
This event does not specify what about the data set has changed,
forcing any observers to assume that all existing items and structure
may no longer be valid. LayoutManagers will be forced to fully rebind
and relayout all visible views.
All you need to do is loop through every position and if needed update desired item otherwise do nothing or skip.
What you should do:
As you are updating your whole view first you need to compare your (visible) adapter's List<Asset> with new List<Asset> and retrieve only those items which you need to be update, once you have the list loop through updated list and update your adapter's view by using viewAdapter.notifyItemChanged(position).
Perform the action to update the adapter as:
public void refreshAssetList(List<Asset> newAssetList){
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
recyclerViewAdapter.setAssetList(newAssetList);
recyclerViewAdapter.notifyDataSetChanged();
}
});
}
After some research I found DiffUtil class for updating list.
From the documentation :
DiffUtil is a utility class that can calculate the difference between
two lists and output a list of update operations that converts the
first list into the second one.
DiffUtil needs new list and old list. It only updates changing items in list. I created AssetDiffUtil class as below :
public class AssetDiffUtil extends DiffUtil.Callback {
private final List<Asset> oldList;
private final List<Asset> newList;
public AssetDiffUtil(List<Asset> oldList, List<Asset> newList) {
this.oldList = oldList;
this.newList = newList;
}
#Override
public int getOldListSize() {
return oldList.size();
}
#Override
public int getNewListSize() {
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final Last oldItem = oldList.get(oldItemPosition).getLast();
final Last newItem = newList.get(newItemPosition).getLast();
return oldItem.getDate().equals(newItem.getDate());
}
#Nullable
#Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
// Implement method if you're going to use ItemAnimator
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
Then I added swapItems method for refreshing list in AssetListRecyclerViewAdapter class.
public void swapItems(List<Asset> newAssetList) {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AssetDiffUtil(this.assetList, newAssetList));
this.assetList.clear();
this.assetList.addAll(newAssetList);
diffResult.dispatchUpdatesTo(this);
}
That's it. But my problem still exist. Before using DiffUtil freezing time 4 seconds . Now, after using DiffUtil freezing time is 2 seconds. Unfortunately this is not a definitive solution for my problem.
use notifyItemRangeChanged instead of notifyDataSetChanged , also sometimes when using progress bars libraries like SpinKitView it increases the freezing time
loadMoreProgress.visibility = View.VISIBLE
homeViewModel.latestBars.observe(viewLifecycleOwner, Observer {
latestBarTests.addAll(it)
latestBarsAdapter.notifyItemRangeChanged(
latestBarTests.size - it.size,
latestBarTests.size
)
})

How to implement Infinite Scrolling with RecyclerView?

I have a recycler and inside of it there are cardviews where I fetch information from a REST service, I'm trying to implement an endless scroll, It's supposed that user will see 10 cardviews every time he scrolls down until there are no more cardviews to show, How can I achieve that?
I've seen a few examples but none of them really helped me about how to do it. I don't even know what I need to put in adapter.class or in my Fragment.class because I don't understand how to implement that, it would be great if someone could tell me the correct way to implement the infinite scroll in my code...
Thanks in advance.
MainAdapter.class
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> implements View.OnClickListener
{
private ArrayList<Business> businessList;
private Activity activity;
private int layoutMolde,idb;
public MainAdapter(Activity activity, ArrayList<Business> list, int layout)
{
this.activity = activity;
this.businessList = list;
layoutMolde = layout;
}
#Override
public MainAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_row, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.mTitle.setText(businessList.get(position).getBusiness_name());
holder.number_rating.setText(businessList.get(position).getRating().toString());
Glide.with(activity).load(businessList.get(position).getLogo_url_string()).into(holder.mImg);
}
#Override
public int getItemCount() {
return businessList.size();
}
#Override
public void onClick(View v)
{
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTitle;
public ImageView mImg;
public ImageView logo;
public RatingBar main_rating;
public TextView number_rating;
public ViewHolder( View itemView)
{
super(itemView);
mTitle = (TextView) itemView.findViewById(R.id.nom_business_main);
number_rating = (TextView) itemView.findViewById(R.id.number_rating);
mImg = (ImageView) itemView.findViewById(R.id.img_main);
main_rating=(RatingBar) itemView.findViewById(R.id.rating_main);
main_rating.setRating((float)1);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v)
{
Intent in = new Intent(v.getContext(), BusinessPremium.class);
int position = getAdapterPosition();
idb = businessList.get(position).getId();
in.putExtra("no", idb);
v.getContext().startActivity(in);
}
});
}
}
}
FeedsFragment.class
public class FeedsFragment extends Fragment
{
private ArrayList<Business> arrayBusiness,arrayBasics;
private Gson gson;
private static final Type BUSINESS_TYPE = new TypeToken<ArrayList<Business>>() {}.getType();
private RecyclerView.LayoutManager mLayoutManager;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View android = inflater.inflate(R.layout.fragment_feeds, container, false);
if (!internetConnectionCheck(FeedsFragment.this.getActivity()))
{
Toast.makeText(getApplicationContext(), "Error de Conexión", Toast.LENGTH_LONG).show();
}
new RequestBase(getActivity()) {
#Override
public JsonObject onHttpOk(JsonObject response) throws JSONException {
JsonObject objeto, pagination_details = null, details, premium_img;
JsonArray data;
if (getActivity() == null)
return response;
if (response.get("pagination") == null)
{
objeto = response;
} else {
objeto = response;
pagination_details = response.get("pagination").getAsJsonObject();
data = objeto.get("data").getAsJsonArray();
gson = new Gson();
arrayBusiness = gson.fromJson(data, BUSINESS_TYPE);
Log.d("size", String.valueOf(arrayBusiness.size()));
FeedsFragment.this.getActivity().runOnUiThread(new Runnable()
{
#Override
public void run()
{
RecyclerView recycler = (RecyclerView) FeedsFragment.this.getActivity().findViewById(R.id.recycler_main);
MainAdapter adapter = new MainAdapter(getActivity(), arrayBusiness, R.layout.main_row);
recycler.setNestedScrollingEnabled(false);
mLayoutManager = new GridLayoutManager(FeedsFragment.this.getActivity(), 2);
recycler.setLayoutManager(mLayoutManager);
recycler.setAdapter(adapter);
GifTextView loading = (GifTextView)FeedsFragment.this.getActivity().findViewById(R.id.loading);
TextView loadingText = (TextView)FeedsFragment.this.getActivity().findViewById(R.id.loadingText);
loading.setVisibility(View.GONE);
loadingText.setVisibility(View.GONE);
}
});
}
if (pagination_details.isJsonNull()) {
Log.d("Paginacion", pagination_details.toString());
}
return objeto;
}
#Override
public void onHttpCreate(JsonObject response) throws JSONException
{
}
#Override
public void onHttpUnprocessableEntity(JsonObject response) throws JSONException
{
this.cancel(true);
final String error = response.get("errors").toString();
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
Toast.makeText(getActivity().getApplicationContext(), error, Toast.LENGTH_LONG).show();
}
});
}
}.execute("businesses/premiums", "GET");
return android;
}
}
you can refresh using SwipeRefreshLayout in android to refresh and in the on refresh override method call your api
note:put your API call request in a method and call that method inyour onRefresh method of SwipeRefreshLayout
When writing RecyclerView.Adapter, you anyway need to provide the getItemCount method that returns the correct number of items (may be large). RecyclerView will call on its own initiative the onBindViewHolder(holder, position) method of this adapter. All you need is to provide functionality of retrieving data, relevant to this position. There is no difference at all, if your list is smaller than screen, slightly larger than screen or Integer.MAX_VALUE size. RecyclerView will take care not to fetch/allocate too much extra items.
You do not need to implement scroll listeners or otherwise explicitly handle the scrolling.
The only tricky part is that you may need to take a long action like server call to get some items. Then just return uninitialized holder (empty view) on the first invocation and start fetching the needed row in the background thread. When you have it, call notifyDataSetChanged or notifyItemRangeChanged, and RecyclerView will take care to update itself.
For performance reasons I would strongly recommend to update content in chunks of the fixed size rather than sending individual server request per every row displayed. For some public servers like Google Books this is clearly a requirement, as they have quota limits per request.
If you need to view the full source code on how this possibly could be implemented, there is an open source project here in GitHub.
Make a static boolean variable named "ready" and initialize it to false.
Add the if ready condition in the onLoadMore method as below.
public boolean onLoadMore(int page, int totalItemsCount) {
if (ready) {
//load more from API
}
return false;
}
set ready to true in onBindViewHolder when the position of item is last.
Here is a way that a colleague of mine introduced. we worked in it together and i implemented it successfully with no issues. I wanted to give back to anyone having this issue.
in your adapter you need to set the count to be infinite size and then when you want the position of an item you should use val loopPos = position % dataSource.size anytime you need the position. lets take a look how this can be done in a recyclerView adapter but could also be applied to FragmentStatePagerAdapter.
class InfiniteLoopingHorizontalRecyclerViewAdapter(var dataSource: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflatedView: View = LayoutInflater.from(parent.context)
.inflate(R.layout.your_finite_layout, parent, false)
return ItemHolder(inflatedView)
}
override fun getItemCount(): Int {
return Integer.MAX_VALUE //***** this should be high enough - wink wink ******
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
//****** this is critical here when you need the position use the loopPos ****/
val loopPos = position % dataSource.size
(holder as? ItemHolder)?.bind(dataSource[loopPos], loopPos)
}
inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(myString: String, position: Int) = with(itemView) {
myTextView.setText(myString)
}
}
}
how it works:
lets say your dataSource size is 50 but your position is at 51 that means the following: 51%50 . which gives you position 1. and lets say again your position is 57 and again your dataSource size is still 50. that means your position is 7. so to be clear, anytime you need a infinite affect you can use the modules of the position and the dataSource size.
ps:
lets go crazy and say we scrolled to position 11323232323214 then that means 11323232323214%50 = 14 so its position 14 in your datasource that will be used. you can then polish off the affect with wrapping your recyclerview in a SnapHelper class
You can add a scrollListener to your recyclerview.
Check a similar answer here
And the main SO post here
Where, the scrollListener will check where exactly are you in the recyclerview and based on some logic (which you can flexibly write) make a second call!

Are there any Observers written In RecyclerView.Adapter to know if dataset has been changed?

I have implemented my RecyclerView with it's Custom Adapter as follows
Global Declarations as follows
private LinearLayoutManager linearLayoutManager;
private int pastVisibleItems, visibleItemCount, totalItemCount;
private CustomRecyclerViewAdapter customRecyclerViewAdapter;
First I created Adapter Instance inside onCreate() method which has Empty Array inside it and set it to recyclerView
linearLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(linearLayoutManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
Utility.ItemDecorationConst);
recyclerView.addItemDecoration(dividerItemDecoration);
customRecyclerViewAdapter = new CustomRecyclerViewAdapter(getActivity());
recyclerView.setAdapter(customRecyclerViewAdapter);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
visibleItemCount = linearLayoutManager.getChildCount();
totalItemCount = linearLayoutManager.getItemCount();
pastVisibleItems = linearLayoutManager.findFirstVisibleItemPosition();
if (loading) {
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
loading = false;
customRecyclerViewAdapter.addProgressBarEntry();
controller.getNextPage(PublisherAppContainerFragment.this);
}
}
}
});
After rendering complete View when I get data from AsyncTask for filling in recyclerView
I call following method of the Adapter to fill data
customRecyclerViewAdapter.addAll(myArray);
note : addAll() is not any overridden method
following is code of my CustomRecyclerViewAdapter
class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.ViewHolder> {
ArrayList<MyModel> arrayList = new ArrayList<>();
Context context;
public CustomRecyclerViewAdapter(Context context) {
this.context = context;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder = null;
//inflated some view
return viewHolder;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
//binded data to holder
}
#Override
public int getItemCount() {
return arrayList.size();
}
public void addAll(ArrayList myArray) {
this.arrayList.addAll(myArray)
}
public void clear() {
arrayList.clear();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public CardView cardView;
public ViewHolder(View view) {
super(view);
this.cardView = (CardView) view.findViewById(R.id.card_view);
this.cardView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
//handle operations
}
}
}
So whenever I get data from AsynTask I call method addAll() and recyclerView works like charm.
Now, My question is how it's working very well even though I have never called notifyDataSetChanged() on the adapter. Are there any previously registered Observers for the adapter? who observes if the dataset which has been returned in public int getItemCount() has been changed?
As I have read from documentation
void notifyDataSetChanged ()
Notify any registered observers that the data set has changed.
that means even though there are some observers registered you need to notify them using notifyDataSetChanged(). Right?
I also called
boolean flag = customRecyclerViewAdapter.hasObservers();
to know if there are any observers registered? Flag is True.
So anyone would please help me understand how exactly these things work?
If you look at the source of RecyclerView setAdapter call you will find a method setAdapterInternal(adapter, false, true);which is responsible for
Replaces the current adapter with the new one and triggers listeners.
This method is responsible for swapping the old adapter with the new one and internally it also registers for the custom Data Observer. This is the reason you are getting the flag as true
Based on what I can see of your code, I would say that there are not any observers attached to your RecyclerView that are picking up changes and keeping the list updated. What is more likely is that you are just getting "lucky" as when you scroll through the list the layout manager is continually calling getItemCount() on the adapter to determine if it should show more items. Whenever you call addAll(), you silently update the item count and it just happens to appear that observers were notified of the changes.
This is definitely a bug, and you would more likely see its effects in your implementation if you were dependent on a particular observer to monitor some aspect of the list, or doing more than just appending new items to the bottom (for example altering or inserting between existing items). The correct implementation as you pointed out is to call notifyDataSetChanged() whenever the list is updated, or even better be more specific with what changed if you can. For example, you can use:
public void addAll(ArrayList myArray) {
int positionStart = getItemCount() - 1;
this.arrayList.addAll(myArray);
notifyItemRangeInserted(positionStart, myArray.size());
}
public void clear() {
int oldSize = getItemCount();
arrayList.clear();
notifyItemRangeRemoved(0, oldSize);
}
My question is how it's working very well even though I have never called notifyDataSetChanged() on the adapter
It's because the addAll method by default calls the notifyDataSetChanged().
public void addAll(T ... items) {
synchronized (mLock) {
if (mOriginalValues != null) {
Collections.addAll(mOriginalValues, items);
} else {
Collections.addAll(mObjects, items);
}
}
if (mNotifyOnChange) notifyDataSetChanged();
}
And
public void addAll(#NonNull Collection<? extends T> collection) {
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.addAll(collection);
} else {
mObjects.addAll(collection);
}
}
if (mNotifyOnChange) notifyDataSetChanged();
}
Here's the link - https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ArrayAdapter.java
EDIT - I see that you have your own addAll method which is calling addAll method of ArrayList.
This is how addAll method works -
private ArrayList<String> ex1 = new ArrayList();
private ArrayList<String> ex2 = new ArrayList();
private ArrayList<String> ex3 = new ArrayList();
ex1.add("one");
ex2.add("two");
ex3.addAll(ex1);
ex3.addAll(ex2);
System.out.println(ex3);
OUTPUT - [one, two]
This is what happening in your case.
I have shown progress bar and once I fetch data I hide the progress bar and make recyclerView visible - If in layout or code you set RecyclerView visibility GONE then layout will not happen and that is why Adapter.getItemsCount() not get called. So if you fetch data and populate adapter array with it and then change RecyclerView visibility from GONE to VISIBLE it will trigger update.
In case you don't call notifyDataSetChanged() RecyclerView will not know about update. I guess there is something else in your code that trigger RecyclerView update. To clarify this behavior let's use some dummy adapter:
private class DummyViewHolder extends RecyclerView.ViewHolder {
public DummyViewHolder (View itemView) {
super(itemView);
}
}
private class Adapter extends RecyclerView.Adapter<DummyViewHolder> {
private int mDummySize = 5;
#Override
public DummyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.dummy_view, parent, false);
return new DummyViewHolder(view);
}
#Override
public void onBindViewHolder(DummyViewHolder holder, int position) {
}
void setSize(int size) { this.mDummySize = size; }
#Override
public int getItemCount() {
return mDummySize;
}
}
And in onCraete() :
ViewHolder v = ...
final Adapter adapter = ..
...
//postpone adapter update
(new Handler()).postDelayed(new Runnable() {
#Override
public void run() {
adapter.setSize(10);//and nothing happend only 5 items on screen
}
}, 5000);

Large number of items in RecyclerView.Adapter - Memory Issue

Overview: I'm having a chat application. Till now, I was using CursorAdapter with a Listview to load my chat items in the list. But now, I'm planning to refactor the code to use RecyclerView with RecyclerView.Adapter and a "Load More" functionality like whatsapp.
Issue: Memory consumption. With CursorAdapter, items not in viewable area were getting Garbage Collected, but now since I'm using an ArrayList of my CustomModal, once you load all the items in the list (by clicking on the "Load More" button) I'm seeing high memory consumption in the memory logs (No Garbage Collection).
My guess is now, I'm loading all the items in an ArrayList and that is causing the issue. Is that it?
Is there a way to avoid the issue or optimize the problem?
EDIT:
Can't post the complete code here, but here is a snippet of the kind of Adapter that I've implemented:
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MyViewHolder> {
private ArrayList<MyModal> mMyModals;
public MessageAdapter(ArrayList<MyModal> mMyModals) {
this.mMyModals = mMyModals;
//... Some fields initialization here
}
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals = myModals;
//... Some fields initialization here
notifyDataSetChanged();
}
public void toggleLoadMore(boolean isLoadMoreEnabled){
if(isLoadMoreEnabled){
//..Checks if load more is already enabled or not
//..If not then enables it by adding an item at 0th poition of MyModal list
//..Then notifyDataSetChanged()
}else{
//..Checks if load more is already disabled or not
//..If not then disables it by removing an item at 0th poition of MyModal list
//..Then notifyDataSetChanged()
}
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder messageViewHolder = null;
View itemLayoutView = null;
MyModal.MessageType messageType = MyModal.MessageType.getMessageTypeFromValue(viewType);
switch (messageType){
case MESSAGE_TYPE1:
itemLayoutView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout1, null);
messageViewHolder = new Type1ViewHolder(itemLayoutView);
break;
case MESSAGE_TYPE2:
itemLayoutView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout2, null);
messageViewHolder = new Type2ViewHolder(itemLayoutView);
break;
}
return messageViewHolder;
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final MyModal myModal = mMyModals.get(position);
MyModal.MessageType messageType = myModal.getMessageType();
holder.initialize(myModal);
}
#Override
public int getItemCount() {
return (mMyModals != null)?mMyModals.size():0;
}
#Override
public int getItemViewType(int position) {
return mMyModals.get(position).getMessageType().getValue();
}
public abstract class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemLayoutView) {
super(itemLayoutView);
}
public abstract void initialize(MyModal myModal);
}
class Type1ViewHolder extends MyViewHolder {
//...Variables
public Type1ViewHolder(View itemLayoutView) {
super(itemLayoutView);
//...variables initialization here
}
#Override
public void initialize(MyModal myModal) {
//...Setting values in view using myModal
}
}
class Type2ViewHolder extends MyViewHolder {
//...Variables
public TextViewHolder(View itemLayoutView) {
super(itemLayoutView);
//...variables initialization here
}
#Override
public void initialize(MyModal myModal) {
//...Setting values in view using myModal
}
}
}
First of all :
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals = myModals;
//... Some fields initialization here
notifyDataSetChanged();
}
Here you are creating a new arraylist and assigning it to your mMyModals. This means there are 2 arraylists at this point and they take up twice the amount of space than required. GC doesnt work the way you expect it to. Since the arraylist is initialized in your activity it will persist as long as the arraylist persists and so will the initial arraylist.
Instead of creating a new arraylist in your activity and passing it to changeList. Just clear your old arraylist and pass that.And also in adapter changeList method you can do the below
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals.clear();
this.mMyModels.addAll(myModels);
//... Some fields initialization here
notifyDataSetChanged();
}
Please let me know if i am not clear. Also show your activity code if this does not work.
Instead of replacing the whole ArrayList and calling notifyDataSetChanged, try adding the items to the ArrayList and then call notifyItemRangeInserted(int positionStart, int itemCount), maybe that could work. Also, you dont have to replace the Adapter's ArrayList. Your Activity/Fragment probably has the same ArrayList, just editing this list in your Activity/Fragment and then calling notifyItemRangeInserted(int positionStart, int itemCount) should do the trick. Also, instead of retrieving all the messages, you could also try to only get the next X amount of messages, so you wont retrieve the messages you already retrieved before (if you didn't do that already).

Categories

Resources