how to update recyclerview after pulling data from server? - android

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.

Related

How to stop the RecyclerView OnBindViewHolder() function iterating through all items of my ArrayList every time a new item is added?

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);}

RecyclerView scrolls down on data update using Room and PagingListAdapter

I created an example project to show an issue I am been struggling with using RecyclerView, Room and Paging, which is that the RecyclerView is unexpectedly scrolling down on data updating.
https://github.com/HappyPeng2x/RoomRecyclerViewExample
The app has a Room database, and I am using an adapter derived from PagedListAdapter to display its values in a RecyclerView.
The query is observed as shown by the code below so that every update of the table will be reflected by the adapter.
PagedList.Config plConfig =
new PagedList.Config.Builder().setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setPageSize(20).build();
new LivePagedListBuilder<>
(mDB.getMyDao().getAllPaged(), plConfig)
.build()
.observe(this, new Observer<PagedList<MyEntry>>() {
#Override
public void onChanged(PagedList<MyEntry> myList) {
adapter.submitList(myList);
}
});
For testing I populate the table with 1 000 key/value pairs. The keys start at 1 and end at 1 000, and the values are all initiated as INITIAL.
I include in each displayed element a toggle button; clicking on it will toggle the value from INITIAL to FINAL and reverse.
When pressing the toggle button at the 155th element, the displayed value changes from INITIAL to FINAL without any issue.
When doing the same operation at the 243rd element, pressing the button causes the RecyclerView to scroll down, which is not expected.
The issue repeats itself each time a button is pressed around this position.
I took a video capture so that the issue can be observed.
https://github.com/HappyPeng2x/RoomRecyclerViewExample/blob/master/videos/device-2019-02-02-105434.webm
I have been struggling quite a bit with this issue, and feel a bit ashamed because it seems like a basic use of Architecture Components, so I would be really happy to get any help.
This issue is a bug, that has been solved by Google and the fix should be included in the next release of Paging, which has unfortunately not happened yet.
The URL in the issue tracker is https://issuetracker.google.com/issues/123834703 - however as it is necessary to be logged to see it I will copy the main elements here.
As the developer explains it, the problem is that the library typically triggers an initial load around the last accessed location, but when placeholders are disabled, this logic is bypassed so the load occurs at the last accessed location. Since this location is often the last partially-offscreen item bound by the RecyclerView, that means most of the items onscreen are missing from the initial load, and only paged in later.
No release has been published containing this fix yet, you have to use a development version, but you can also add a workaround which was suggested by the developer for my example app at https://github.com/HappyPeng2x/RoomRecyclerViewExample .
1) Add the following class to MainActivity.java:
static class RoomFactoryWrapper<T> extends DataSource.Factory<Integer, T> {
final DataSource.Factory<Integer, T> m_wrappedFactory;
RoomFactoryWrapper(#NonNull Factory<Integer, T> wrappedFactory) {
m_wrappedFactory = wrappedFactory;
}
#NonNull
#Override
public DataSource<Integer, T> create() {
return new DataSourceWrapper<>((PositionalDataSource<T>) m_wrappedFactory.create());
}
static class DataSourceWrapper<T> extends PositionalDataSource<T> {
final PositionalDataSource<T> m_wrappedSource;
DataSourceWrapper(PositionalDataSource<T> wrappedSource) {
m_wrappedSource = wrappedSource;
}
#Override
public void addInvalidatedCallback(#NonNull InvalidatedCallback onInvalidatedCallback) {
m_wrappedSource.addInvalidatedCallback(onInvalidatedCallback);
}
#Override
public void removeInvalidatedCallback(
#NonNull InvalidatedCallback onInvalidatedCallback) {
m_wrappedSource.removeInvalidatedCallback(onInvalidatedCallback);
}
#Override
public void invalidate() {
m_wrappedSource.invalidate();
}
#Override
public boolean isInvalid() {
return m_wrappedSource.isInvalid();
}
#Override
public void loadInitial(#NonNull LoadInitialParams params,
#NonNull LoadInitialCallback<T> callback) {
// Workaround for paging bug: https://issuetracker.google.com/issues/123834703
// edit initial load position to start 1/2 load ahead of requested position
int newStartPos = params.placeholdersEnabled
? params.requestedStartPosition
: Math.max(0, params.requestedStartPosition - (params.requestedLoadSize / 2));
m_wrappedSource.loadInitial(new LoadInitialParams(
newStartPos,
params.requestedLoadSize,
params.pageSize,
params.placeholdersEnabled
), callback);
}
#Override
public void loadRange(#NonNull LoadRangeParams params,
#NonNull LoadRangeCallback<T> callback) {
m_wrappedSource.loadRange(params, callback);
}
}
}
2) Use the wrapper on the dataSourceFactory:
new LivePagedListBuilder<>
(new RoomFactoryWrapper<>(mDB.getMyDao().getAllPaged()), plConfig)
Though I am not confident but this might be help you.
PagedList.Config plConfig =
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(30)
.setPageSize(50).build();
I set this config and it works great.
The main item there is setMaxSize(pageSize + 2 * prefetchDistance)
in minimum possible value.
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setMaxSize(pageSize + 2 * prefetchDistance)
.setPrefetchDistance(prefetchDistance)
.setPageSize(pageSize)
.build()`
I tried without it and RecyclerView also worked visually fine but PagedListAdapter incorrectly bound elements.

How to color/grey out individual RecyclerView rows (via link to SharedPreferences)

I'm currently writing a major upgrade to my language teaching App, and wanted to use SharedPreferences in order to store user progress. Basically, a recyclerview displays a list of topics, which lead to exercises -complete 5 exercises, and the topic is finished.
At this point, I call saveProgress(), and store the topic number and an int representing completion (1) in sharedPreferences.
However, how and where in the recylclerView code should I call the check for a topic being completed? Currently in my bindData method of my ViewHolder inner class, I have:
SharedPreferences sp = getActivity().getSharedPreferences("phraseprefs", Context.MODE_PRIVATE);
//the number topic serves as the key name of the preference
int completed = sp.getInt(Integer.toString(mTopic.topicRef), 0);
if (completed ==1){
//we check if exercise completed, if so, make view greyed out
float alpha= 0.65f;
nameTextView.setAlpha(alpha);
nameTextView.setBackgroundColor(Color.GREY);
itemView.setAlpha(alpha);
itemView.setBackgroundColor(Color.GREY);
}
This doesnt seem to work however. Any tips would be appreciated!
EDIT: Added in SaveProgress() method below, note that "topic" in this context is a number from 0-xxx, at increments of 5 per topic (i.e. topic 0,5,10, with exercises 0-4,5-9,10-14, respectively)
public void saveProgress(){
SharedPreferences sp = getActivity().getSharedPreferences("phraseprefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
//we use 1 to represent completion instead of boolean because we can also sum these numbers
//for a completion percentage if necessary
editor.putInt(Integer.toString(topic), 1);
editor.commit();
}
EDIT2: Solved by a mixture of trial and error and magic.
First, initialize the textViews in ViewHolder constructor.
Second, Fetch sharedPreferences in OnBindViewHolder
Third, call methods to change the textViews within onBindViewHolder, provided the shared Preference meets your conditions.
Thank you all
I think RecyclerView shouldn't know about SharedPreferences at all. You probably have a model class Topic which contains all the information about a topic. You can include your completion int in this class. Something like this.
class Topic {
/* all topic data */
private int completion;
public int getCompletetion() {
return compeletion;
}
public void setCompletion(int completion) {
this.completion = completion;
}
public Topic(/*Other Topics fields*/) {
}
public Topic(/*Other Topic fields*/, int completion) {
/* initialization of other fields */
this.completion = completion;
}
}
Then you can load and set a completion int for each Topic in your array (I mean List or whatever container you use). Something like this
List<Topic> topicsList = new ArrayList<>();
/* add your topics to the list from whatever source */
for (Topic topic : topicsList) {
topicsList.setCompletion(loadCompletion(topic));
}
YourAdapter adapter = new YourAdapter(topicsList)
/* ... */
Now, in order to check completion int for a Topic in your data array in your RecyclerView you can just call topic.getCompletion(). It would look something like this
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
Topic topic = topicsList.get(position);
int completion = topic.getCompletion();
switch (completion) {
case '0':
holder.modifyViewForCompletionZero();
break;
case '1':
holder.modifyViewForCompletionOne();
break;
/* Call holder methods for other completion states */
}
}
And your inner ViewHolder would contain methods for adjusting view for each state.

Changing a business object in an Adapter

I'm building an android app similar to Facebook (gets a newsfeed stored in db from a REST API), and I am now implementing a reddit-like voting system, in which every feed item has a vote state (none, up, down) stored in db.
When I do retrieve the newsfeed json, i set its voteState to the vote state in the json, then I display it through a newsfeed adapter.
But if i set the voteState in the newsfeed adapter to another value (based on an onClickListener), this changed value does not reach the actual newsfeed.
This is because every time I scroll the newsfeed, the newsfeed adapter gets a new instance of the newsfeed, and doesn't care about the value I changed.
Thus, I'm looking for the best way to modify permanently the newsfeed vote state from the newsfeed adapter (this can be generalized to any variable assigned to an ArrayList and then displayed through an ArrayListAdapter).
If you have any suggestions, please feel free to respond.
Thank you :)
EDIT :
public class NewsfeedAdapter extends RecyclerView.Adapter<NewsfeedAdapter.ViewHolder> {
private Newsfeed newsfeed;
private FeedItem item;
public NewsfeedAdapter(..., Newsfeed newsfeed) {
...;
this.newsfeed = newsfeed;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected Button button_up/down....
public ViewHolder(View v) {
super(v);
button = findViewById ... (R.id.button);
...
}
}
// Create new views (invoked by the layout manager)
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
...
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
final FeedItem item = newsfeed.get(position);
holder.button_up.setTag(position);
holder.button_up.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
changeVote(item, "up");
}
});
holder.button_down.setTag(position);
holder.button_down.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
changeVote(item, "down");
}
});
}
public void changeVote(FeedItem item, String flag) {
// Log.d(TAG, "State of vote :" + item.getVoteState());
if (item.getVoteState().equals(flag)) {
item.setVoteState("none");
Log.d(TAG, "Deleting vote :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Deleting vote...").execute(String.valueOf(item.getId()), flag, "delete");
}
else if (item.getVoteState().equals("none")){
item.setVoteState(flag);
Log.d(TAG, "Voting :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Voting...").execute(String.valueOf(item.getId()), flag, "insert");
} else {
item.invertVoteState(flag);
Log.d(TAG, "Inverting :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Voting...").execute(String.valueOf(item.getId()), flag, "invert");
}
} }
You have two options:
1) Send updates to the rest API every time a piece of data is changed. This is really expensive and not something you want to do most likely.
2) Create a local data layer between your rest API and your listview - most likely an SQLite database or some other ORM lib. This way you can hold an instance of the DB in your adapter, and whenever a piece of data changes, you can update that through a simple db method call, and then periodically send updates to the rest API, while also checking for new updates from the rest API and then pushing them to the DB.
I would highly recommend using an SQLite DB, as you can keep a counter in your DB code of a certain number of changes, or time passed since last sync, and keep the data the user is seeing up to date with out smashing the users data usage with repeated network calls.

notifyDataSetChanged not working on RecyclerView

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);

Categories

Resources