I come to you today with three simple tips that will guarantee to improve your Codename One apps’ usability:
- Don’t block the EDT
- If you must block the EDT, do it when the user won’t notice
- Prefer asynchronous coding patterns (e.g. using callbacks) to synchronous coding patterns (e.g. *AndWait(), and *AndBlock() methods).
The first one is GUI 101. All of the UI is drawn on the EDT (Event dispatch thread). If you are performing a slow operation on the EDT, the user will probably notice a lag or a jerk in the UI because drawing can’t take place while your slow operation is occupying the thread.
Here’s a quick example. I have a form that allows the user to swipe through 12 images, which are loaded from the classpath (getResourceAsStream()). My first attempt at this is to load all of the images into Labels inside the Form’s constructor as follows:
public class MyForm extends Form {
private Tabs tabs;
public MyForm() {
super("Welcome to My App");
tabs = new Tabs();
tabs.hideTabs();
setScrollable(false);
Container buttonsContentWrapper = new Container(new BorderLayout());
try {
for (int i=1; i< =12; i++) {
int w = calculateTheWidth();
int h = calculateTheHeight();
Button l = new Button(
Image.createImage(
Display.getInstance().getResourceAsStream(null, "/Instructions"+fi+".png")
).scaledSmallerRatio(w, h)
);
l.setUIID("Label");
if (i>1) {
l.setUIID("InstructionImage");
}
l.addActionListener(e->{
buttonsContentWrapper.setVisible(!buttonsContentWrapper.isVisible());
revalidate();
});
Container tabWrapper = FlowLayout.encloseCenter(l);
tabWrapper.getAllStyles().setPaddingTop(Display.getInstance().convertToPixels(2));
if (i==12) {
tabWrapper.putClientProperty("lastSlide", Boolean.TRUE);
} else {
tabWrapper.putClientProperty("lastSlide", Boolean.FALSE);
}
tabs.addTab(i+"", tabWrapper);
}
} catch (Exception ex) {
Log.e(ex);
}
Container mainContent = new Container(new BorderLayout());
mainContent.addComponent(BorderLayout.CENTER, tabs);
setLayout(new LayeredLayout());
addComponent(mainContent);
buttonsContentWrapper.setVisible(false);
Button skipButton = new Button("Skip");
skipButton.addActionListener(e->{
MyApp.getInstance().getMainForm().show();
});
Container buttonsContent = FlowLayout.encloseRight(skipButton);
buttonsContentWrapper.addComponent(BorderLayout.SOUTH, buttonsContent);
addComponent(buttonsContentWrapper);
}
}
So what’s the problem with this code? Loading the 12 images inside the constructor of this form takes too long. If I have code like:
MyForm form = new MyForm();
form.show();
There is a lag of 1 or 2 seconds in the new MyForm()
line — before the form is even shown. This feels really bad to the user.
We can improve on this by employing the 2nd tip above:
If you must block the EDT (hey we need to load the images sometime right?), then do it when the user won’t notice
Rather than loading all of the images directly inside the MyForm
constructor, we can load each one inside its own Display.callSerially()
dispatch, as shown below:
public class MyForm extends Form {
private Tabs tabs;
public MyForm() {
//...
try {
for (int i=1; i< =12; i++) {
//...
final Button l = new Button();
Display.getInstance().callSerially(()->{
try {
l.setIcon(
Image.createImage(
Display.getInstance().getResourceAsStream(null, "/Instructions"+fi+".png")
).scaledSmallerRatio(fw, fh)
);
l.getParent().revalidate();
} catch (Exception ex){
Log.e(ex);
}
});
//...
}
} catch (Exception ex) {
Log.e(ex);
}
//...
}
}
This will still load the images on the EDT, but it will do them one by one, and in a future event dispatch, so that the code won’t block at all in the constructor. If you run this code, you’ll notice that the 1 to 2 second lag before showing the form is gone. However, the form transition may contain a few “jerks” because it is still interleaving the loading of the images while drawing frames.
So this is an improvement, but still not a good user experience. Luckily we can go back to tip #1, “Don’t block the EDT”, when we realize that we didn’t have to block the EDT at all. We can load the images on a background thread, and then apply them as icons to the labels when they are finished loading, as shown below:
public class MyForm extends Form {
private Tabs tabs;
public MyForm() {
// ...
try {
for (int i=1; i< =12; i++) {
// ...
final Button l = new Button();
Display.getInstance().scheduleBackgroundTask(()->{
try {
Image im = Image.createImage(
Display.getInstance().getResourceAsStream(null, "/Instructions"+fi+".png")
).scaledSmallerRatio(fw, fh);
if (im != null) {
Display.getInstance().callSerially(()->{
l.setIcon(im);
l.getParent().revalidate();
});
}
} catch (Exception ex){
Log.e(ex);
}
});
//...
}
} catch (Exception ex) {
Log.e(ex);
}
//...
}
}
This has double nesting. The first nest (inside scheduleBackgroundTask()) downloads the icon on a background thread. Then the second nesting using callSerially(), assigns the image as the label’s icon back on the EDT. This was necessary because we can’t access the label from the background thread. That part must occur on the EDT. But that part is non-intensive and very fast to perform.
So the result is a very fluid user experience with no lags and no jerks.
Prefer Async to Sync
I’ll address the preference of Async to Sync separately. The example above is a sort of example of this since the nested calls to scheduleBackgroundTask() and callSerially() are technically “callbacks”. However, with this tip I’m more specifically targeting methods like invokeAndBlock()
, addToQueueAndWait()
, and other *AndWait()
methods. At their core, all of these methods are built upon invokeAndBlock()
so I’ll target that one specifically here – and the wisdom gleaned will also apply to all AndWait()
methods.
First of all, if you aren’t familiar with invokeAndBlock
, it is a marvelous invention that allows you to “block” the EDT without actually blocking the EDT. It will indeed block the current dispatch event, but while it is blocked, it will start processing the rest of the events in the EDT queue. That way your app won’t lock up while your code is blocked. This strategy is used for modal dialogs to great effect. You can effectively show a dialog, and the “next” line of code isn’t executed until the user closes the dialog – but the UI itself doesn’t lock up.
invokeAndBlock()
is the infrastructure that allows you to do synchronous network requests on the EDT (e.g. NetworkManager.getInstance().addToQueueAndWait(conn)
). Since this pattern is so convenient (it allows you to think serially about your workflow – which is much easier), it is used in all kinds of places where it really shouldn’t be.
So why NOT use invokeAndBlock
Because it will ALMOST always result in a worse user experience. I’ll illustrate that with a scenario that would seem, at first, to be a good case for invokeAndBlock
(addToQueueAndWait()
).
Here is a form that allows a user to update his bio. Somehow the form needs to be populated with the user’s existing profile data, which is exists on a network server. The question is when and how do we load this data from the server.
A first attempt might populate the data inside the form’s constructor using AddToQueueAndWait() (or some method that encapsulates this). That might look like this:
public class MyForm extends Form {
public MyForm() {
ConnectionRequest req = createConnectionRequest();
NetworkManager.getInstance().addToQueueAndWait(req);
setupFormComponents();
populateFormData();
}
}
The problem with this is similar to our first example loading images from the classpath. Execution will stall inside the constructor for our form while the data is loaded. So the user will have to wait to show the form. A common technique to mitigate this UX blunder is to display an infinite progress indicator so the user knows that something is happening. That’s better, but it still makes the app feel slow.
If we want the user to be able to see the form immediately, then either we need to have loaded the data before-hand, or we need to show the form, and populate it later. We could also use a combination (e.g. show the form with data we loaded before, then update it once we have the new data.
Loading data before hand, exclusively, is not realistic. There must exist a point after which we deem the data is too old and we need to reload it. And we are back at needing to load data when the form loads.
If we want to solve this problem, and still use addToQueueAndWait()
, we either need to wrap addToQueueAndWait()
inside a callSerially()
dispatch so that it doesn’t block inside the constructor – and delay our show()
method; or we need to move the call somewhere else, after the form is already shown. Although that isn’t ideal either, because we’d like to have the data as soon as possible – so the longer we delay the “sending” of the network request, the longer the user has to wait for the result.
Now, our handy tool (invokeAndBlock) that was supposed to reduce our app’s complexity, is actually making it more complex. Wrapping it inside callSerially() in the constructor means that we are now combining an async callback with sync blocking code. We might as well, at that point, just use addToQueue()
, and use a result listener to process the response without blocking the EDT at all, as shown in the example below:
public class MyForm extends Form {
public MyForm() {
ConnectionRequest req = createConnectionRequest();
req.addResponseListener(e->{
populateFormDataWithResponse(req);
});
NetworkManager.getInstance().addToQueue(req);
setupFormComponents();
}
}
This predicament is the reason not to use invokeAndBlock
(addtoQueueAndWait()
). It isn’t that they are evil, or they can’t be made to work. It is because, if you aim to achieve an optimal user experience, it will get in the way more than it will help.
Does this mean that you should never use invokeAndBlock()
or addToQueueAndWait()
? No. There are valid cases for both of these. E.g. addToQueueAndWait()
can be used from a background thread (off the EDT), in which case it isn’t even using invokeAndBlock
. It is just plain-old blocking that background thread, which is perfectly OK because it’s not the EDT. In addition, there may be cases where you DO want to block the flow of the application without locking up the UI. Modal dialogs is the flag-ship use-case for this. I struggle to think of another suitable scenario though.