Home > Software engineering >  Android RecyclerView only contain one item after adding items got from asynchronous retrofit calls
Android RecyclerView only contain one item after adding items got from asynchronous retrofit calls

Time:05-22

I have a recycler view for image display with a simple adapter.

public class ImageListAdapter extends RecyclerView.Adapter<ImageListAdapter.SingleItemRowHolder> {

    private ArrayList<Bitmap> itemsList;

    public ImageListAdapter(ArrayList<Bitmap> itemsList) {
        this.itemsList = itemsList;
    }

    @Override
    public SingleItemRowHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.image_card, null);
        return new SingleItemRowHolder(v);
    }

    @Override
    public void onBindViewHolder(SingleItemRowHolder holder, int i) {
        Bitmap img = itemsList.get(i);
        holder.itemImage.setImageBitmap(img);
    }

    @Override
    public int getItemCount() {
        return (null != itemsList ? itemsList.size() : 0);
    }

    public void addItem(Bitmap image) {
        itemsList.add(image);
        notifyItemInserted(itemsList.size()-1);
    }

    public void setItemsList(ArrayList<Bitmap> bmps) {
        itemsList = bmps;
    }

    public ArrayList<Bitmap> getItemsList() {
        return itemsList;
    }

    public void cleanData() {
        itemsList = new ArrayList<>();
        notifyDataSetChanged();
    }

    public class SingleItemRowHolder extends RecyclerView.ViewHolder {

        protected ImageView itemImage;

        public SingleItemRowHolder(View view) {
            super(view);
            this.itemImage = (ImageView) view.findViewById(R.id.itemImage);
        }
    }
} 

Nothing complex here. Just a simple adapter that getting bitmap list as input and show them in the recycle view.

The point is, the bitmaps are asynchronously added via a retrofit call, as shown in the folloing:

googleMapService.getImageByPhotoReference(s, 300, 300, apiKey).enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        if (response.isSuccessful()) {
            if (response.body() != null) {
                Bitmap bmp = BitmapFactory.decodeStream(response.body().byteStream());
                adapter.addItem(bmp);
            } 
        } 
    }
});

As you can see here, the bitmap is added to the adapter and the adapter should be able to notify the changes as addItem() contains a notifyItemInserted() call. And this retrofit call is called several times depending on how many times the view model has changes its value (there is a observer observing the change of view model's value. Once the value changes, the above retrofit will get called). So the code that changes view model value look like this:

if (placeDetail.getPhotoRef() != null) {
    for (PlaceDetail.PhotoRef ref : placeDetail.getPhotoRef()) {
         viewModel.setPhoto(ref.getPhotoRef()); // ref.getPhotoRef returns the photo ref string
     }
}

In my expectation, the item list of the adapter should now contains several images. I tried to debug it, and I found when the addItem() method get called, the amount of item list did change as expected. But when it came into the onBindViewHolder() callback, there is only one item left in the item list, which is the first bitmap.

Can anyone tell me why this issue happened? Any suggestion would be appreciated.

---------------------------------- Update --------------------------------

Interesting. I finally found why it only shows one photo. As you can see here, the adapter (id:24856) item list size is 4, which is in my expectation.

enter image description here

However, there is another adapter(id:24962) exists. For that adapter, the add item method only called once thus only one item in that adapter. When the recycler view changes the content, the onBindViewHolder is called on the second adapter. Now my question is, where does the second adapter come from ???

enter image description here

Here is my code to initialize recycler view and adapter

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {

        binding = PlaceFragmentBinding.inflate(getLayoutInflater());

        RecyclerView recyclerView = binding.imageGallery;
        recyclerView.setHasFixedSize(true);
        ImageListAdapter adapter = new ImageListAdapter(new ArrayList<>());
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
        recyclerView.setAdapter(adapter);

        return binding.getRoot();
    }

CodePudding user response:

Hej AhSoHard,

  • did you check if it is a timing issue, because you are loading your data asynchronously and it could lead to missing updates.

  • also could it be that setItemsList(ArrayList<Bitmap> bmps) is called and overwrites the current list?

  • is there a reason why you do not use the viewGroup as a base for inflating the layout resource in onCreateViewHolder()?

LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.image_card, viewGroup, false)
  • in my opinion the Adapter should not be the holder of the current state, maybe have a list outside and use a ListAdapter to display it. When you have a new list of bitmaps, you just call adapter.submit(bitmaps) and the new list will be displayed. Together with a DiffCallback, the old items will not update and the new ones added at the end of the list.

CodePudding user response:

Let's implement the DiffUtils adapter and try to change your calls in this way. As kotlin works as interoperable with java, therefore I am mentioning the gist for an adapter that is written in kotlin.

Check adapter and view holder implementation

Code

ImageListAdapter adapter;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = PlaceFragmentBinding.inflate(getLayoutInflater());
    View view = binding.getRoot();
    setContentView(view);
 }

@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

    binding.imageGallery.setHasFixedSize(true);
    adapter = new ImageListAdapter(new ArrayList<>());
    binding.imageGallery.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
    binding.imageGallery.setAdapter(adapter);

    return binding.getRoot();
}

Interface googleMapService

 Single<ResponseBody> getImageByPhotoReference(_,_,_,_)

Network Call

 googleMapService.getImageByPhotoReference(s, 300, 300, apiKey)
     .subscribeOn(Schedulers.io())
     .observeOn(AndroidSchedulers.mainThread())
     .subscribeWith(new DisposableSingleObserver<ResponseBody>() {
          @Override
          public void onSuccess(ResponseBody response) {
               if (response != null) {
                     Bitmap bmp = BitmapFactory.decodeStream(response.body().byteStream());
                     adapter.addItem(bmp);
               } 
          }

          @Override
          public void one rror(Throwable e) {
                    // Do with your error
          }
  });

CodePudding user response:

It's interesting that you initialize the adapter in onCreateView method and it has local visibility(only visible inside of the onCreateView method). But in the googleMapService.getImageByPhotoReference() method's callback you are referencing it adapter.addItem(bmp);. So my guess is that you are referencing the wrong adapter object in the googleMapService.getImageByPhotoReference() method's callback. The callback may reference an old instance of ImageListAdapter.

Try to initialize ImageListAdapter adapter as a property of your Fragment class and see if it works.

  • Related