Why notifyItemChanged only works in post Runnable after setAdapter?
Asked Answered
O

2

8
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    RecyclerView list = (RecyclerView) findViewById(R.id.list);
    list.setLayoutManager(new LinearLayoutManager(this));
    final RecyclerView.Adapter adapter = getAdapter();
    list.setAdapter(adapter);

    adapter.notifyItemChanged(1, new Object());//this doesn't work
    list.post(new Runnable() {
        @Override
        public void run() {
            adapter.notifyItemChanged(1, new Object());//this works
        }
    });


}

@NonNull
private RecyclerView.Adapter getAdapter() {
    return new RecyclerView.Adapter() {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new ItemViewHolder( LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.list_view_item, parent, false));
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            ItemViewHolder item = (ItemViewHolder) holder;
            item.tv.setText("test");
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
            if(payloads.isEmpty())
                onBindViewHolder(holder, position);
            else{
                ItemViewHolder item = (ItemViewHolder) holder;
                item.tv.setText("changed!!!!!! ");
            }
        }

        @Override
        public int getItemCount() {
            return 40;
        }

        class ItemViewHolder extends RecyclerView.ViewHolder{
            TextView tv;
            ItemViewHolder(View itemView) {
                super(itemView);
                tv = (TextView) itemView.findViewById(R.id.tv);
            }
        }
    };
}}

very simple example here just to test notifyItemChanged, it works only when post to the Message queue, but not by invoking directly after setAdapter. setAdapter triggers a call to requestlayout(), does that mean notifyItemChanged can't happen if it is in middle of laying out items?

Osber answered 2/2, 2017 at 0:55 Comment(0)
O
8

After a bit of investigation it turns out that notifyItemChanged only works when RecyclerView is attached and actually has completed onLayout which happens after onCreate.

internally during RecyclerView.onLayout() which is called by notifyItemChanged -> requestLayout(), processAdapterUpdatesAndSetAnimationFlags() is called which checks if the item that is to be updated has an available ViewHolder which in this case is null because during onCreate(), RecyclerView is not attached to the window, thus no measurement and layout has done to RecyclerView

Apparently complete drawing of RecyclerView happens sometime after onCreate() and OnResume()

@Override
protected void onResume() {
    super.onResume();
    boolean a = list.isAttachedToWindow();//this is false!
}

So to make notifyItemChanged work on onCreate

list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            adapter.notifyItemChanged(1, new Object());//this will update the item
        }
    });



new Thread((new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(28);//small amount of delay,below 20 doesn't seem to work
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    adapter.notifyItemChanged(1, new Object());//also works
                }
            });
        }
    })).start();

I guess the reason for this is ViewRootImpl.performTraversal() is controlled by the system and it happens sometime after DecorView is attached to the window which is during onCreate

I'm assuming this can only happen during onCreate and onResume, it may not happen if called later.

Osber answered 2/2, 2017 at 11:17 Comment(1)
Nice research, thank you! layout change listener seems like the way to go, but I believe you want to remove the listener right after you notify the item, otherwise it will be notified on every layout changeLargely
L
1

Message queue, but not by invoking directly after setAdapter. setAdapter triggers a call to requestlayout(), does that mean notifyItemChanged can't happen if it is in middle of laying out items?

setAdapter synchronously adds a message to the UI message queue to render items. But the message will be handled later, not when you call setAdapter.

If you call notifyItemChanged right away, then it will be called before actual rendering because the message generated by setAdapter hasn't been dispatched yet.

On the other hand, putting it in its own message will do the same thing as setAdapter does - it will synchronously generate a message which will be dispatched later. Thus, the message will be dispatched after the one that was sent by setAdapter and that's why it works.

Largely answered 2/2, 2017 at 1:24 Comment(9)
Thanks for the response, I was thinking the same , but all I can find is requestlayout() when setAdapter() is called, does requestLayout() only tells the View to re-measure and draw itself ?, maybe I'm wrong, where is the sourcecode that shows setAdapter() is posting to UI message queue?Osber
Yes, but re-measuring and re-drawing the view works through the message queue, so requestLayout() only schedules this process. Each view has a UI handler attached. If no handler is attached at the moment then all this rendering logic is scheduled via RunQueue class, you can take a look here: grepcode.com/file/repository.grepcode.com/java/ext/…Largely
Hi, thanks for that, really helpful. But i'm still a bit confused about If you call notifyItemChanged right away, then it will be called before actual rendering because the message generated by setAdapter hasn't been dispatched yet., why is that the case?Osber
Because you are currently dispatching the "onCreate" message taken from the message queue. If you don't post notifyItemChanged to the message queue, then the order will look like this: 1. dispatch "onCreate", 1.1 post "setAdapter" message to the queue, 1.2 call notifyItemChanged, 2. dispatch "setAdapter" message. In this case notifyItemChanged was called too earlyLargely
Now, if you post it to the message queue, the order will be like this: 1. dispatch "onCreate", 1.1 post "setAdapter" message to the queue, 1.2 post "notifyItemChanged" message to the queue, 2. dispatch "setAdapter" message, 3. dispatch "notifyItemChanged" message. In this case it works as expected because you want to notify the adapter when it's ready, not while you are creating the activity.Largely
Just to clarify, by "post setAdapter message" I mean, call setAdapter which calls requestLayout() which posts rendering message to the queue.Largely
I think notifyItemChanged calls requestLayout() as well, why isn't this post to the queue after the setAdater->requestLayout() call?Osber
For that I don't have an answer, sorry. My guess: when you call notifyItemChanged on the adapter object, it delegates the call to a list of observers listening to the change. It might be the case that observers are not yet attached because it happens after requestLayout() finished the setup of the adapter into recycler view. This is just a guess, feel free to continue your research from here :) The fact that posting the message to the queue makes a difference, I think, proves that there is no posting inside the method call. Otherwise it would work without it, like any other method call.Largely
I have updated my answer, see if it make sense to you, thanks.Osber

© 2022 - 2024 — McMap. All rights reserved.