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?
Snackbar in android is a new widget introduced with the Material Design library as a replacement of a Toast. Android Snackbar is light-weight widget and they are used to show messages in the bottom of the application with swiping enabled. Snackbar android widget may contain an optional action button.
Run this Android Application on a physical Android Device or Android Virtual Device, and you get an Activity with the button as shown in the following screenshot. When you click on the “Show Snackbar” button, a Snackbar appears at the bottom of the screen. An action could be set to Snackbar using Snackbar.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With