'Android RecyclerView only contain one item after adding items got from asynchronous retrofit calls
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.
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 ???
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();
}
Solution 1:[1]
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 inonCreateViewHolder()
?
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 calladapter.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.
Solution 2:[2]
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 onError(Throwable e) {
// Do with your error
}
});
Solution 3:[3]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Daniel Knauf |
Solution 2 | Ali Azaz Alam |
Solution 3 |