I have made quite a lot of various Swing apps and their loading time usually vary between just a few seconds and minutes depending on application UI/data size. Also in some cases application data loading is mixed with UI loading.
A few seconds load time is not an issue, but when it is longer than let's say 10 seconds - it is obvious that some kind of loading screen should be displayed until UI/data is fully initialized.
What you would usually do - create some kind of loading screen first (for example window with logo and some label that is being updated while application is loading) and update it from various loading "points" in your application.
The problem is - application is usually loaded within single call queued into EDT and it is hard to separate it into multiply calls to EDT without complicating the application's code. So since application loading is performed in a single call queued to EDT you simply cannot update the loading screen properly - update won't be displayed until the application is initialized since the EDT is busy with loading the application.
So to implement loading screen in some cases I have moved application UI initialization outside of the EDT and have designed them in the way that it won't do any UI update stuff while loading is performed. Application's frame display and all application UI actions would still be performed in EDT. This is not too good generally, but after lots of testing and looking through the Swing code I know for sure that it doesn't cause any issues even on large applications. Still, that isn't a good to do generally even if it doesn't cause any issues.
So the question is: What approaches can be used to display and update application loading screen properly while keeping application initialization in EDT?
Hopefully it isn't too broad.
Here is a "dummy" application that showcases a "bad" approach:
import javax.swing.*;
import java.awt.*;
public class DummyApplication extends JFrame
{
private static JDialog loadingDialog;
private static JLabel loadingProgress;
public DummyApplication ()
{
super ( "Dummy application" );
dummyProgressUpdate ( "Loading content...", 3000 );
final JLabel label = new JLabel ( "Custom content" );
label.setBorder ( BorderFactory.createEmptyBorder ( 100, 100, 100, 100 ) );
getContentPane ().add ( label );
dummyProgressUpdate ( "Loading settings...", 3000 );
setDefaultCloseOperation ( WindowConstants.EXIT_ON_CLOSE );
pack ();
setLocationRelativeTo ( null );
dummyProgressUpdate ( "Opening application...", 1000 );
}
private static void dummyProgressUpdate ( final String status, final int time )
{
SwingUtilities.invokeLater ( () -> loadingProgress.setText ( status ) );
dummyLoadTime ( time );
}
private static void dummyLoadTime ( final long time )
{
try
{
Thread.sleep ( time );
}
catch ( final InterruptedException e )
{
e.printStackTrace ();
}
}
public static void main ( final String[] args ) throws Exception
{
// Displaying loading screen from EDT first
SwingUtilities.invokeAndWait ( () -> {
loadingDialog = new JDialog ( ( Window ) null, "Loading screen" );
loadingProgress = new JLabel ( "Initializing application...", JLabel.CENTER );
loadingProgress.setBorder ( BorderFactory.createLineBorder ( Color.LIGHT_GRAY ) );
loadingDialog.getContentPane ().setLayout ( new BorderLayout () );
loadingDialog.getContentPane ().add ( loadingProgress );
loadingDialog.setUndecorated ( true );
loadingDialog.setAlwaysOnTop ( true );
loadingDialog.setModal ( false );
loadingDialog.setSize ( 400, 100 );
loadingDialog.setLocationRelativeTo ( null );
loadingDialog.setVisible ( true );
} );
// Initializing application outside of the EDT
final DummyApplication applicationFrame = new DummyApplication ();
// Displaying application from the EDT
SwingUtilities.invokeLater ( () -> {
loadingDialog.setVisible ( false );
applicationFrame.setVisible ( true );
} );
}
}
Also some time ago I found this interesting stuff implemented in JDK7:
http://sellmic.com/blog/2012/02/29/hidden-java-7-features-secondaryloop/
That SecondaryLoop
feature allows to block futher code execution in EDT thread without causing the UI to stuck. It is basically the same what modal JDialog does when opened within EDT.
So with this feature I found probably a better way to solve the case:
import javax.swing.*;
import java.awt.*;
public class DummyApplication extends JFrame
{
private static JDialog loadingDialog;
private static JLabel loadingProgress;
public DummyApplication ()
{
super ( "Dummy application" );
dummyProgressUpdate ( "Loading content...", 3000 );
final JLabel label = new JLabel ( "Custom content" );
label.setBorder ( BorderFactory.createEmptyBorder ( 100, 100, 100, 100 ) );
getContentPane ().add ( label );
dummyProgressUpdate ( "Loading settings...", 3000 );
setDefaultCloseOperation ( WindowConstants.EXIT_ON_CLOSE );
pack ();
setLocationRelativeTo ( null );
dummyProgressUpdate ( "Displaying application...", 1000 );
}
private static void dummyProgressUpdate ( final String status, final int time )
{
// Use SecondaryLoop to block execution and force loading screen update
final SecondaryLoop loop = Toolkit.getDefaultToolkit ().getSystemEventQueue ().createSecondaryLoop ();
SwingUtilities.invokeLater ( () -> {
loadingProgress.setText ( status );
loop.exit ();
} );
loop.enter ();
// Perform dummy heavy operation
dummyLoadTime ( time );
}
private static void dummyLoadTime ( final long time )
{
try
{
Thread.sleep ( time );
}
catch ( final InterruptedException e )
{
e.printStackTrace ();
}
}
public static void main ( final String[] args ) throws Exception
{
// Displaying loading screen from EDT first
SwingUtilities.invokeAndWait ( () -> {
loadingDialog = new JDialog ( ( Window ) null, "Loading screen" );
loadingProgress = new JLabel ( "Initializing application...", JLabel.CENTER );
loadingProgress.setBorder ( BorderFactory.createLineBorder ( Color.LIGHT_GRAY ) );
loadingDialog.getContentPane ().setLayout ( new BorderLayout () );
loadingDialog.getContentPane ().add ( loadingProgress );
loadingDialog.setUndecorated ( true );
loadingDialog.setAlwaysOnTop ( true );
loadingDialog.setModal ( false );
loadingDialog.setSize ( 400, 100 );
loadingDialog.setLocationRelativeTo ( null );
loadingDialog.setVisible ( true );
} );
// Initializing and displaying application from the EDT
SwingUtilities.invokeLater ( () -> {
final DummyApplication applicationFrame = new DummyApplication ();
loadingDialog.setVisible ( false );
applicationFrame.setVisible ( true );
} );
}
}
As you can see - in dummyProgressUpdate
method I've done some tricky workaround for my case - basically I am blocking execution that performed in EDT and waiting for a separate call queued into EDT that would update loading screen.
And that actually works - though I am not sure that this is a good thing to do and whether it might cause any side-effects on a larger scale. And also the loading screen in that case will only be updated when that force update is made which means that if the loading screen has (for example) some animation running - it won't display properly and will only be updated together with text.
any unwanted side effects for Swing as I haven't tested it properly yet
- by default I'm against the use of SwingWorker(for enjoy, testing shadowing buggy, non_well_designed and documented Future, Ececutor, SwingWorker), for me is Runnable#Thread/util.Timer, for/with one class/void (java.bean.)EventHandler as notifier to Swing that notify EDT with invokeLater, rest of is playing with fire, but here is SwingWorker most popular :-) – Pakistan