I am getting data from server and then parsing it and storing it in a List. I am using this list for the RecyclerView's adapter. I am using Fragments.
I am using a Nexus 5 with KitKat. I am using support library for this. Will this make a difference?
Here is my code: (Using dummy data for the question)
Member Variables:
List<Business> mBusinesses = new ArrayList<Business>();
RecyclerView recyclerView;
RecyclerView.LayoutManager mLayoutManager;
BusinessAdapter mBusinessAdapter;
My onCreateView():
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Getting data from server
getBusinessesDataFromServer();
View view = inflater.inflate(R.layout.fragment_business_list,
container, false);
recyclerView = (RecyclerView) view
.findViewById(R.id.business_recycler_view);
recyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(mLayoutManager);
mBusinessAdapter = new BusinessAdapter(mBusinesses);
recyclerView.setAdapter(mBusinessAdapter);
return view;
}
After getting data from server, parseResponse() is called.
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
mBusinesses.clear();
Business business;
business = new Business();
business.setName("Google");
business.setDescription("Google HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Yahoo");
business.setDescription("Yahoo HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Microsoft");
business.setDescription("Microsoft HeadQuaters");
mBusinesses.add(business);
Log.d(Const.DEBUG, "Dummy Data Inserted\nBusinesses Length: "
+ mBusinesses.size());
mBusinessAdapter = new BusinessAdapter(mBusinesses);
mBusinessAdapter.notifyDataSetChanged();
}
My BusinessAdapter:
public class BusinessAdapter extends
RecyclerView.Adapter<BusinessAdapter.ViewHolder> {
private List<Business> mBusinesses = new ArrayList<Business>();
// Provide a reference to the type of views that you are using
// (custom viewholder)
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextViewName;
public TextView mTextViewDescription;
public ImageView mImageViewLogo;
public ViewHolder(View v) {
super(v);
mTextViewName = (TextView) v
.findViewById(R.id.textView_company_name);
mTextViewDescription = (TextView) v
.findViewById(R.id.textView_company_description);
mImageViewLogo = (ImageView) v
.findViewById(R.id.imageView_company_logo);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public BusinessAdapter(List<Business> myBusinesses) {
Log.d(Const.DEBUG, "BusinessAdapter -> constructor");
mBusinesses = myBusinesses;
}
// Create new views (invoked by the layout manager)
#Override
public BusinessAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
Log.d(Const.DEBUG, "BusinessAdapter -> onCreateViewHolder()");
// create a new view
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_business_list, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
Log.d(Const.DEBUG, "BusinessAdapter -> onBindViewHolder()");
Business item = mBusinesses.get(position);
holder.mTextViewName.setText(item.getName());
holder.mTextViewDescription.setText(item.getDescription());
holder.mImageViewLogo.setImageResource(R.drawable.ic_launcher);
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
Log.d(Const.DEBUG, "BusinessAdapter -> getItemCount()");
if (mBusinesses != null) {
Log.d(Const.DEBUG, "mBusinesses Count: " + mBusinesses.size());
return mBusinesses.size();
}
return 0;
}
}
But I don't get the data displayed in the view. What am I doing wrong?
Here is my log,
07-14 21:15:35.669: D/xxx(2259): Dummy Data Inserted
07-14 21:15:35.669: D/xxx(2259): Businesses Length: 3
07-14 21:26:26.969: D/xxx(2732): BusinessAdapter -> constructor
I don't get any logs after this. Shouldn't getItemCount() in adapter should be called again?
In your parseResponse() you are creating a new instance of the BusinessAdapter class, but you aren't actually using it anywhere, so your RecyclerView doesn't know the new instance exists.
You either need to:
Call recyclerView.setAdapter(mBusinessAdapter) again to update the RecyclerView's adapter reference to point to your new one
Or just remove mBusinessAdapter = new BusinessAdapter(mBusinesses); to continue using the existing adapter. Since you haven't changed the mBusinesses reference, the adapter will still use that array list and should update correctly when you call notifyDataSetChanged().
Try this method:
List<Business> mBusinesses2 = mBusinesses;
mBusinesses.clear();
mBusinesses.addAll(mBusinesses2);
//and do the notification
a little time consuming, but it should work.
Just to complement the other answers as I don't think anyone mentioned this here: notifyDataSetChanged() should be executed on the main thread (other notify<Something> methods of RecyclerView.Adapter as well, of course)
From what I gather, since you have the parsing procedures and the call to notifyDataSetChanged() in the same block, either you're calling it from a worker thread, or you're doing JSON parsing on main thread (which is also a no-no as I'm sure you know). So the proper way would be:
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
// <yadda yadda yadda>
mBusinessAdapter = new BusinessAdapter(mBusinesses);
// or just use recyclerView.post() or [Fragment]getView().post()
// instead, but make sure views haven't been destroyed while you were
// parsing
new Handler(Looper.getMainLooper()).post(new Runnable() {
public void run() {
mBusinessAdapter.notifyDataSetChanged();
}
});
}
PS Weird thing is, I don't think you get any indications about the main thread thing from either IDE or run-time logs. This is just from my personal observations: if I do call notifyDataSetChanged() from a worker thread, I don't get the obligatory Only the original thread that created a view hierarchy can touch its views message or anything like that - it just fails silently (and in my case one off-main-thread call can even prevent succeeding main-thread calls from functioning properly, probably because of some kind of race condition)
Moreover, neither the RecyclerView.Adapter api reference nor the relevant official dev guide explicitly mention the main thread requirement at the moment (the moment is 2017) and none of the Android Studio lint inspection rules seem to concern this issue either.
But, here is an explanation of this by the author himself
I had same problem. I just solved it with declaring adapter public before onCreate of class.
PostAdapter postAdapter;
after that
postAdapter = new PostAdapter(getActivity(), posts);
recList.setAdapter(postAdapter);
at last I have called:
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
// Display the size of your ArrayList
Log.i("TAG", "Size : " + posts.size());
progressBar.setVisibility(View.GONE);
postAdapter.notifyDataSetChanged();
}
May this will helps you.
Although it is a bit strange, but the notifyDataSetChanged does not really work without setting new values to adapter. So, you should do:
array = getNewItems();
((MyAdapter) mAdapter).setValues(array); // pass the new list to adapter !!!
mAdapter.notifyDataSetChanged();
This has worked for me.
Clear your old viewmodel and set the new data to the adapter and call notifyDataSetChanged()
In my case, force run #notifyDataSetChanged in main ui thread will fix
public void refresh() {
clearSelection();
// notifyDataSetChanged must run in main ui thread, if run in not ui thread, it will not update until manually scroll recyclerview
((Activity) ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
adapter.notifyDataSetChanged();
}
});
}
I always have this problem that I forget that the RecyclerView expects a new instance of a List each time you feed the adapter.
List<X> deReferenced = new ArrayList(myList);
adapter.submitList(deReferenced);
Having the "same" List (reference) means not declaring "new" even if the List size changes, because the changes performed to the List also propagates to other Lists (when they are simply declared as this.localOtherList = myList) emphasis on the keyword being "=", usually components that compare collections make a copy of the result after the fact and store it as "old", but not Android DiffUtil.
So, if a component of yours is giving the same List each and every time you submit it, the RecyclerView won't trigger a new layout pass.
The reason is that... AFAIR, before the DiffUtil even attempts to apply the Mayers algorithm, there is a line doing a:
if (newList == mList)) {return;}
I am not sure how much "good practice" does de-referencing within the same system is actually defined as "good" ...
Specially since a diff algorithm is expected to have a new(revised) vs old(original) component which SHOULD in theory dereference the collection by itself after the process has ended but... who knows...?
But wait, there is more...
doing new ArrayList() dereferences the List, BUT for some reason Oracle decided that they should make a second "ArrayList" with the same name but a different functionality.
This ArrayList is within the Arrays class.
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {#link Collection#toArray}. The returned list is
* serializable and implements {#link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* #param <T> the class of the objects in the array
* #param a the array by which the list will be backed
* #return a list view of the specified array
*/
#SafeVarargs
#SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a); //Here
}
This write-through is funny because if you:
Integer[] localInts = new Integer[]{1, 2, 8};
Consumer<List<Integer>> intObserver;
public void getInts(Consumer<List<Integer>> intObserver) {
this.intObserver = intObserver;
dispatch();
}
private void dispatch() {
List<Integer> myIntegers = Arrays.asList(localInts);
intObserver.accept(myIntegers);
}
... later:
getInts(
myInts -> {
adapter.submitList(myInts); //myInts = [1, 2, 8]
}
);
Not only does the List dispatched obeys the dereferencing on each submission, but when the localInts variable is altered,
public void set(int index, Integer value) {
localInts[index] = value;
dispatch(); // dispatch again
}
...
myModel.set(1, 4) // localInts = [1, 4, 8]
this alteration is also passed to the List WITHIN the RecyclerView, this means that on the next submission, the (newList == mList) will return "false" allowing the DiffUtils to trigger the Mayers algorithm, BUT the areContentsTheSame(#NonNull T oldItem, #NonNull T newItem) callback from the ItemCallback<T> interface will throw a "true" when reaching index 1. basically, saying "the index 1 inside RecyclerView (that was supposed to be 2 in th previous version) was always 4", and a layout pass will still not perform.
So, the way to go in this case is:
List<Integer> trulyDereferenced = new ArrayList<>(Arrays.asList(localInts));
adapter.submitList(trulyDereferenced);
Related
I'm trying to make a simple chat application for my own learning - no firebase involved (the messages won't be stored between sessions). I've implemented a RecyclerView to show all the messages. The problem is that every time I add a new message, the RecyclerView Adapter will iterate through all previous messages before populating the latest one. Whilst this isn't causing any major bugs, it does seem very inefficient. The relevant functions in my adapter class are shown below:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
MessageItem newMsgItem = messages.get(position);
holder.txtMsgContent.setText(newMsgItem.getMsgContent());
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) holder.msgParentView.getLayoutParams();
if (newMsgItem.isSent()) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
holder.msgParentView.setLayoutParams(layoutParams);
holder.msgParentView.setCardBackgroundColor(0xFF03DAC5);
} else {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
holder.msgParentView.setLayoutParams(layoutParams);
holder.msgParentView.setCardBackgroundColor(0xFF67706F);
}
}
// boolean sent: false = received, true = sent
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyDataSetChanged();
}
I could implement a condition-check like below, but that isn't a satisfying solution as it only masks the problem - i.e. the program is still iterating unnecessarily through all previous messages:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
if (position == messages.size() - 1) {
//... do function
}
}
Is there a way to make the program only call onBindViewHolder for the newest item that's been added? I also saw this forum, but as I'm a beginner I couldn't tell if they were having the same issue as me.
RecyclerView populating each item every time i add a new item
notifyDataSetChanged() always reloads the whole view. Use notifyItemInserted() instead.
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyItemInserted(messages.size()-1);
}
Don't use notifyDataSetChanged() method, you can use notifyItemInserted() method, this will not refresh every time.
public void addMessage (boolean sent, String msgContent) {
messages.add(new MessageItem(sent, msgContent));
notifyItemInserted(messages.size()-1);}
I am currently trying to unit test recyclerview addonitemclick listner, with either junit or mockito. here's my code:
private void mypicadapter(TreeMap<Integer, List<Photos>> photosMap) {
List<PhotoListItem> mItems = new ArrayList<>();
for (Integer albumId : photosMap.keySet()) {
ListHeader header = new ListHeader();
header.setAlbumId(albumId);
mItems.add(header);
for (Photos photo : photosMap.get(albumId)) {
mItems.add(photo);
}
pAdapter = new PhotoViewerListAdapter(MainActivity.this, mItems);
mRecyclerView.setAdapter(pAdapter);
// set 5 photos per row if List item type --> header , else fill row with header.
GridLayoutManager layoutManager = new GridLayoutManager(this, 5);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
if (mRecyclerView.getAdapter().getItemViewType(position) == PhotoListItem.HEADER_TYPE)
// return the number of columns so the group header takes a whole row
return 5;
// normal child item takes up 1 cell
return 1;
}
});
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.addOnItemTouchListener(new PhotoItemClickListener(MainActivity.this,
new PhotoItemClickListener.OnItemClickListener() {
#Override
public void onItemClick(View view, int position) {
if (pAdapter.getItemViewType(position) == PhotoListItem.HEADER_TYPE) return;
Photos photo = pAdapter.getItem(position);
Intent intent = new Intent(MainActivity.this, DetailViewActivity.class);
intent.putExtra(PHOTO_DETAILS, photo);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
MainActivity.this,
new Pair<>(view.findViewById(R.id.photoItem),
getString(R.string.transition_name_photo))
);
ActivityCompat.startActivity(MainActivity.this, intent, options.toBundle());
}
}));
}
Is there a way I can unit test : addOnItemTouchListener or OnItemClickListener/onitemclick ,mock the functionality etc. I am pretty new to unit testing and been looking up online at a couple of tutorials and pretty confused. Any step by step tutorial for testing functions or any suggestions would help.Also, any other possible unit testable scenarios in this function would be helpful. Thanks!
In unit tests it's improtant to have small, testable chunks of code, I would rather have 10 methods with single resposinbility than one method for all actions.
All used inputs should be delivered as parameters to method, than you test if at given input you will receive expected output.
Don't test what you don't own - testing of View's onClick() is part of AOSP job. You can test how you react to onClickListener.
You should have class under test that handles the logic. Than in your test you instantiate this class to test it and mock everything else (usually good way to go is to pass dependencies through constructor)
Example:
So that way if you have method like
goToDetailActivity(Photo photo){...}
I would wrap it in interface, lets call it View. In View you put also all other methods that your logic must call and are view related like interacting with view components, navigation etc.
Than you should have your logic class, lets call it Presenter:
public class Presenter {
Presenter(View:view) {
this.view = view;
}
public void onPhotoClicked(Photo:photo) {
if (shouldDetailScreenBeOpened())
view.goToDetailActivity(Photo photo);
else view.showError("error");
}
private boolean shouldDetailScreenBeOpened() {
// do caclualtions here
...}
}
I treat my adapters as part of view, so it has no real logic. So to pass clicks to Presenter you should pass it through activity/fragment (View implementation) to Presenter (if someone is fun of RxJava, RxBinding library can be used) and call it's onPhotoClicked(photo) method.
And in testing you have to mock things that you need (and are not subjects to test):
View view= Mockito.mock(View.class);
Presenter tested = Presenter(view);
Photo validPhoto = Mockitio.mock(Photo.class);
Mockito.when(validPhoto.getUrl()).thanReturn("image.com")
//call method which will be triggered on item click
tested.onPhotoClicked(validPhoto)
//Check if method was invoked with our object
Mockito.verify(view).goToDetailActivity(validPhoto);
//Check also not so happy path
Photo invalidPhoto = Mockitio.mock(Photo.class);
Mockito.when(invalidPhoto.getUrl()).thanReturn(null)
//call method which will be triggered on item click
tested.onPhotoClicked(invalidPhoto)
Mockito.verify(view,never()).goToDetailActivity(invalidPhoto);
Mockito.verify(view).showError("error")
Good staring point vogella mokcito tutorial.
I would probably extract the anonymous inner class you're creating in addOnItemTouchListener into a separate class.
Then I would write the relevant (unit) tests for the onItemClick method.
This very much depends however on the overall context of your application and what exactly it is that you want to test.
The discussion about unit tests versus integration tests is pretty expensive and there is also some confusion and different opinions around what really is a unit test.
I would recommend starting to read more on the topic from Martin Fowler's excellent series of articles - e.g. https://martinfowler.com/bliki/UnitTest.html
There's also another piece about test doubles in general, that should guide you with regards to whether you want to be using mocks or stubs: https://martinfowler.com/articles/mocksArentStubs.html
currently I am using FirebaseRecyclerAdapter to represent data on a RecyclerView using the following Firebase query:
postsQuery = mDatabase.child("lists_new”).orderByKey().limitToFirst(10);
My RecyclerView has a header with 2 buttons: New List, Old List.
New list is loaded by default, and my question is, when the user taps the Old List button, what is the most efficient way to replace the new list with old list.
The Firebase query for the old list looks like this:
postsQuery = mDatabase.child("lists_old”).orderByKey().limitToFirst(10);
Note: both new list and the old list has the same data types, i.e. they share the same Java POJO class and the the same layout.
You will need a new adapter and attach that to the same RecyclerView.
So after constructing the new query, you create a new adapter and attach it to the view:
adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(
Chat.class, android.R.layout.two_line_list_item, ChatHolder.class, postsQuery) {
recyclerView.setAdapter(adapter);
I have a similar need and a similar solution except I am calling cleanup() on the adapter before new'ing up another one. I am thinking without calling cleanup() it will create a leak of adapters and/or listeners?
In onActivityCreated() in my Fragment I am calling a method in the Fragment that manages the recycler view. Call the method to initialize or refresh the list and pass in a leaf node name. If the adapter is not null then call cleanup(). Create a new database reference by concatenating a new leaf node with the parent reference, new-up a new adapter and set it.
I call cleanup() in onDestroy() as well, per usual.
It works fine so far though I've only tested using the emulator and a small data set.
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
refreshList(newLeafNode);
}
#Override
public void onDestroy() {
super.onDestroy();
mRVAdapter.cleanup();
}
protected void refreshList(String newLeafNode) {
if (mRVAdapter != null) {
mRVAdapter.cleanup();
}
DatabaseReference newDbRef = mParentRef.child(newLeafNode);
mRVAdapter = new FirebaseRecyclerAdapter<String, MyViewHolder>(String.class, R.layout.single_row, MyViewHolder.class, newDbRef) {
#Override
protected void populateViewHolder(MyViewHolder viewHolder, String model, int position) {
viewHolder.setImage(model);
}
};
mMyRV.setAdapter(mRVAdapter);
}
I am using a singleton for fetching data from a web service and storing the resulting data object in an ArrayList. It looks like this:
public class DataHelper {
private static DataHelper instance = null;
private List<CustomClass> data = null;
protected DataHelper() {
data = new ArrayList<>();
}
public synchronized static DataHelper getInstance() {
if(instance == null) {
instance = new DataHelper();
}
return instance;
}
public void fetchData(){
BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions options = new QueryOptions();
options.setSortBy(Arrays.asList("street"));
query.setQueryOptions(options);
CustomClass.findAsync(query, new AsyncCallback<BackendlessCollection<CustomClass>>() {
#Override
public void handleResponse(BackendlessCollection<CustomClass> response) {
int size = response.getCurrentPage().size();
if (size > 0) {
addData(response.getData());
response.nextPage(this);
} else {
EventBus.getDefault().post(new FetchedDataEvent(data));
}
}
#Override
public void handleFault(BackendlessFault fault) {
EventBus.getDefault().post(new BackendlessFaultEvent(fault));
}
});
}
public List<CustomClass> getData(){
return this.data;
}
public void setData(List<CustomClass> data){
this.data = data;
}
public void addData(List<Poster> data){
this.data.addAll(data);
}
public List<CustomClass> getData(FilterEnum filter){
if(filter == FilterEnum.NOFILTER){
return getData();
}else{
// Filtering and returning filtered data
}
return getData();
}
}
The data is fetched correctly and the list actually contains data after it. Also, only one instance is created, as intended. However, whenever I call getData later, the length of this.data is 0. Because of this I also tried it with a subclass of Application holding the DataHelper object, resulting in the same problem.
Is there a good way of debugging this? Is there something like global watches in Android Studio?
Is there something wrong with my approach? Is there a better approach? I am mainly an iOS developer, so Android is pretty new to me. I am showing the data from the ArrayList in different views, thus I want to have it present in an the ArrayList as long as the application runs.
Thanks!
EDIT: Example use in a list view fragment (only relevant parts):
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
filter = FilterEnum.NOFILTER;
data = DataHelper.getInstance().getData(filter);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
customClassListAdapter = new customClassListAdapter(getActivity(), data);}
EDIT2: Added code where I fetch the data from Backendless, changed reference of DataHelper to reference of data in first EDIT
EDIT3: I usa a local EventBus for notifying the list view about the new data. This looks like this and works (initially the data gets populated, but after e.g. applying a filter, the ArrayList I get with getData is empty):
#Subscribe
public void onMessageEvent(FetchedDataEvent event) {
customClassListAdapter.notifyDataSetChanged();
}
Try instead of keeping reference to your DataHelper instance, keeping reference to your list of retrieved items. F.e. when you first fetch the list (and it's ok as you say), assign it to a class member. Or itarate through it and create your own array list of objects for future use.
Okay I finally found the problem. It was not about the object or memory management at all. Since I give the reference on getData to my ArrayAdapter, whenever I call clear (which I do when changing the filter) on the ArrayAdapter, it empties the reference. I basically had to create a copy of the result for the ArrayAdapter:
data = new ArrayList<>(DataHelper.getInstance().getData(filter));
I was not aware of the fact that this is a reference at all. So with this the data always stays in the helper entirely. I only did this because this:
customClassListAdapter.notifyDataSetChanged();
does hot help here, it does not call getData with the new filter again.
Thanks everyone for your contributions, you definitely helped me to debug this.
It is likely that getData does get called before the data is filled.
A simple way to debug this is to add (import android.util.Log) Log.i("MyApp.MyClass.MyMethod", "I am here now"); entries to strategic places in fetchData, addData and getData and then, from the logs displayed by adb logcat ensure the data is filled before getData gets called.
If i had to send a request to get data within onBindViewHolder(), how can i update view after data comes back from server?
what I have now is that I cache the data along with row position, so that next time when user scrolls to that row, I can display info right away.
but there are 2 other issues I don't know how to solve.
I scroll the list to view item at position 10, 11 and 12. I decided to wait for data to come back. do i call notifyDataSetChanged() after? because onBindViewHolder already been called by the time data comes back and view would just remained empty, but I also don't think by calling notifyDataSetChanged() after each request would be a good idea.
I start to view the list at position 0 and keep scrolling to position 10. app sends out request to pull data for position 0 to 10. since the request at 0 is sent out first, more likely it would get back first or at least sooner than position 10, but by that time I'm already viewing the item at position 10. my view would start changing if all requests are back in order, so it would show data for position 0 then keep updating all the way to 10.
is it a bad practice to load data from server as recyclerview scrolls? but by doing this would save me a lot of time, and I guess for user too? because instead of sending all the requests ahead of time, user get to see partial data while other data are being loaded in the background.
Thanks!!!
EDITED
public class TestAdapter extends RecyclerView.Adapter<TestAdapter.ViewHolder> {
private Context ctx;
private ArrayList<Photo> alPhotos = new ArrayList<>();
private HashMap<String, Drawable> hmImages = new HashMap<>();
public TestAdapter(Context ctx, ArrayList<Photo> alPhotos) {
this.ctx = ctx;
this.alPhotos = alPhotos;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photo_brief, parent, false));
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Photo photo = alPhotos.get(position);
loadRemoteImage(photo.IMG, holder.ivThumb, true);
holder.tvEmail.setText(photo.EMAIL);
}
#Override
public int getItemCount() {
return alPhotos.size();
}
private void loadRemoteImage(final String imgUrl, final ImageView view, final boolean cache) {
if (hmImages.containsKey(imgUrl)) {
view.setImageDrawable(hmImages.get(imgUrl));
} else {
final WeakReference<ImageView> weakView = new WeakReference<>(view);
RequestManager.getManager().add(new Request<>(imgUrl, new DrawableParser(), new RequestCallback<Drawable>() {
#Override
public void onFinished(Request<Drawable> request, Response<Drawable> response) {
if (cache) hmImages.put(imgUrl, response.result);
ImageView view = weakView.get();
if (view != null) {
view.setImageDrawable(response.result);
}
}
#Override
public void onError(Request<Drawable> request, Response<Drawable> response) {
}
#Override
public void onTimeout(Request<Drawable> request, Response<Drawable> response) {
}
})
);
}
}
public class ViewHolder extends RecyclerView.ViewHolder {
private ImageView ivThumb;
private TextView tvEmail;
public ViewHolder(View itemView) {
super(itemView);
findViews();
}
private void findViews() {
ivThumb = (ImageView) itemView.findViewById(R.id.ivThumb);
tvEmail = (TextView) itemView.findViewById(R.id.tvEmail);
}
}
}
It's a good idea to load data while scrolling, a lot of popular apps do that. I personally use WeakReference in this case. I store a weak reference to the view holder in my model when I start loading data. If the reference is still valid by the time I get the response then it makes sense to update the view. If there is no view holder in memory then it's already been recycled and I don't have to update anything anymore.
When onViewRecycled is called you can clear the weak reference and also consider cancelling the network request (depends on your needs).
Caching works perfect with this model, you just insert this logic before making a network request. Again, this depends on your needs, maybe you don't need caching at all, or maybe your data is rarely updated then it makes sense to always use cache until some event.
In my app I also use EventBus, it helps me with event handling, but it is absolutely fine to just use Android SDK and support library.
You can also add a ScrollListener if you need to differentiate the item behavior depending on whether user scrolls the list right now. E.g. in my app I animate the data if list loaded and user wasn't scrolling it (improves interaction with the user). When user scrolls I load data as is, because it will be too much motion on the screen if I animate data.