q126y q126y - 3 months ago 28
Android Question

How does calling Snackbar.make() from non-UI thread work?

I can call

Snackbar.make()
from a background thread without any problems. This is surprising to me since I thought UI operations are only allowed from the UI thread. But that is definitely not the case here.

What exactly makes
Snackbar.make()
different? Why doesn't this cause exceptions like any other UI component when you modify it from a background thread?

Answer

First of all: make() doesn't perform any UI related operations, it just creates a new Snackbar instance. It is the call to show() which actually adds the Snackbar to the view hierarchy and performs other dangerous UI related tasks. However you can do that safely from any thread because it is implemented to schedule any show or hide operation on the UI thread regardless of which thread called show().

For a more detailed answer let's take a closer look at the behaviour in the source code of the Snackbar:


Let's start where it all begins, with your call to show():

public void show() {
    SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}

As you can see the call to show() gets an instance of the SnackbarManager and then passes the duration and a callback to it. The SnackbarManager is a singleton. Its the class which takes care of displaying, scheduling and managing a Snackbar. Now lets continue with the implementation of show() on the SnackbarManager:

public void show(int duration, Callback callback) {
    synchronized (mLock) {
        if (isCurrentSnackbarLocked(callback)) {
            // Means that the callback is already in the queue. We'll just update the duration
            mCurrentSnackbar.duration = duration;

            // If this is the Snackbar currently being shown, call re-schedule it's
            // timeout
            mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
            scheduleTimeoutLocked(mCurrentSnackbar);
            return;
        } else if (isNextSnackbarLocked(callback)) {
            // We'll just update the duration
            mNextSnackbar.duration = duration;
        } else {
            // Else, we need to create a new record and queue it
            mNextSnackbar = new SnackbarRecord(duration, callback);
        }

        if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
            // If we currently have a Snackbar, try and cancel it and wait in line
            return;
        } else {
            // Clear out the current snackbar
            mCurrentSnackbar = null;
            // Otherwise, just show it now
            showNextSnackbarLocked();
        }
    }
}

Now this method call is a little more complicated. I am not going to explain in detail what's going on here, but in general the synchronized block around this ensures thread safety of calls to show().

Inside the synchronized block the manager takes care of dismissing currently shown Snackbars updating durations or rescheduling if you show() the same one twice and of course creating new Snackbars. For each Snackbar a SnackbarRecord is created which contains the two parameters originally passed to the SnackbarManager, the duration and the callback:

mNextSnackbar = new SnackbarRecord(duration, callback);

In the above method call this happens in the middle, in the else statement of the first if.

However the only really important part - at least for this answer - is right down at the bottom, the call to showNextSnackbarLocked(). This where the magic happens and the next Snackbar is queued - at least sort of.

This is the source code of showNextSnackbarLocked():

private void showNextSnackbarLocked() {
    if (mNextSnackbar != null) {
        mCurrentSnackbar = mNextSnackbar;
        mNextSnackbar = null;

        final Callback callback = mCurrentSnackbar.callback.get();
        if (callback != null) {
            callback.show();
        } else {
            // The callback doesn't exist any more, clear out the Snackbar
            mCurrentSnackbar = null;
        }
    }
}

As you can see first we check if a Snackbar is queued by checking if mNextSnackbar is not null. If it isn't we set the SnackbarRecord as the current Snackbar and retrieve the callback from the record. Now something kind of round about happens, after a trivial null check to see if the callback is valid we call show() on the callback, which is implemented in the Snackbar class - not in the SnackbarManager - to actually show the Snackbar on the screen.

At first this might seem weird, however it makes a lot of sense. The SnackbarManager is just responsible for tracking the state of Snackbars and coordinating them, it doesn't care how a Snackbar looks, how it is displayed or what it even is, it just calls the show() method on the right callback at the right moment to tell the Snackbar to show itself.


Let's rewind for a moment, up until now we never left the background thread. The synchronized block in the show() method of the SnackbarManager ensured that no other Thread can interfere with everything we did, but what schedules the show and dismiss events on the main Thread is still missing. That however is going to change right now when we look at the implementation of the callback in the Snackbar class:

private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
    @Override
    public void show() {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
    }

    @Override
    public void dismiss(int event) {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
    }
};

So in the callback a message is send to a static handler, either MSG_SHOW to show the Snackbar or MSG_DISMISS to hide it again. The Snackbar itself is attached to the message as payload. Now we are almost done as soon as we look at the declaration of that static handler:

private static final Handler sHandler;
private static final int MSG_SHOW = 0;
private static final int MSG_DISMISS = 1;

static {
    sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case MSG_SHOW:
                    ((Snackbar) message.obj).showView();
                    return true;
                case MSG_DISMISS:
                    ((Snackbar) message.obj).hideView(message.arg1);
                    return true;
            }
            return false;
        }
    });
}

So this handler runs on the UI thread since it is created using the UI looper (as indicated by Looper.getMainLooper()). The payload of the message - the Snackbar - is casted and then depending on the type of the message either showView() or hideView() is called on the Snackbar. Both of these methods are now executed on the UI thread!

The implementation of both of these is kind of complicated, so I won't go into detail of what exactly happens in each of them. However it should be obvious that these methods take care of adding the View to the view hierarchy, animating it when it appears and disappears, dealing with CoordinatorLayout.Behaviours and other stuff regarding the UI.

If you have any other questions feel free to ask.


Scrolling through my answer I realize that this turned out way longer than it was supposed to be, however when I see source code like this I can't help myself! I hope you appreciate a long in depth answer, or maybe I might have just wasted a few minutes of my time!

Comments