Swing application initialization and loading screen approach
Asked Answered
A

2

6

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.

Arthrospore answered 15/10, 2014 at 10:15 Comment(8)
I would consider using a SwingWorker to do the majority of heavy lifting, it has progress support and UI synchronisation functionality. You could also use a ExecutorService of some kind, mixed with a SwingWorker to act as the broker to synchronisation with the UI and finally, the done method can be used to close down the splash screen and start the main applicationBirkle
Can't you just use a standard splash screen? docs.oracle.com/javase/tutorial/uiswing/misc/splashscreen.htmlUssery
@Birkle the point is that there is no real "heavy lifting" in case you are loading application with thousands of separate UI elements - each separate element is loaded almost instantly, but altogether that might take some time. Plus some micro-operations and calculations between those components also take a bit of time, but separating each of these operations (which are spreaded along the initialization code) into a separate thread/worker is just totally crazy. So SwingWorker won't really solve this.Arthrospore
@a_horse_with_no_name it might, but first of all - that is just an image and in case I want something complex placed onto the loading screen (or simply Swing-based screen) that won't be possible. Also as far as I have tested SplashScreen causes various issues inside the application after loading on Unix systems so I prefer not using it.Arthrospore
+1 I think that I'm see that somewhere, there is an issue with SecondaryLoop compiled in JDK8 without back compability to Java7, idea is correct but for Java7, somewhere I'm see that, now without, no idea where, as aside I have an issue with SecondaryLoop and (java.bean.)EventHandler, output from SecondaryLoop by default doesn't required invokeLater, but ....Pakistan
@miklegarin So what you're really talking about is lazy loading the UI as needed rate then all at onceBirkle
@Pakistan about the SecondaryLoop - it is actually fine for me if it only works on JDK8+ since we are moving all products to JDK8 support exclusively to cleanup and simplify the code, was just wondering whether or not it might cause any unwanted side effects for Swing as I haven't tested it properly yetArthrospore
@Mikle Garin 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
B
3

If you're loading data or having to connect to remote services or other lengthy applications, then a SwingWorker is the simpler of the available approches, as it provides progress support via its setProgress method and PropertyChangeListener support.

It provides UI synchronisation via it's process/publish methods, allowing you to safely update the UI and the done method and/or the PropertyChangeListener can be used to determine when the worker is done

If all you're doing is trying to loading UI elements then you should consider a lazy loading approach.

So rather then loading ALL the UI elements at once, especially when the user may not actually look at sections of the UI, it is a much better idea to load those elements only when you absolutely have to and save yourself the time.

When using things like CardLayout or JTabbedPane, this is a much better to only initialise the UI and data of the views when they actually become visible.

If you really want, you could also have it unload when the view is no longer visible, disconnecting listeners from the data source, stopping it form responding to changes to the data source that the user will not actually see...

Birkle answered 15/10, 2014 at 19:45 Comment(5)
That might be a good way - I would just postpone the loading of some UI parts to lazy-load them later, that will reduce the initial loading time and almost never will freeze the UI. I guess that is the way IntelliJ IDEA application is loaded looking at how it initializes on slow machines.Arthrospore
@MikleGarin It's a common enough concept, don't need it, don't load it. We do this alot, but we also do it for menus, as they can change context depending on what the user is doing/done...a complete pain in the ... code. iOS does this ALOT behind the scenes (I imagine the Android does to), which makes sense when you have limited resourcesBirkle
@Birkle that is actually a really good tip about menus - was usually keeping them loaded and "up-to-date" which is indeed a pain code-wise.Arthrospore
@MikleGarin It is a constant balancing act between responsive UI and quick loading, sometimes it works for quick loading, sometimes it works for responsiveness, you need to make judgement calls on each part, the same approach might not work all time ;)Birkle
@Birkle true, the whole case is pretty vague and might require different approaches depending on the application specifics, but still thanks for the useful tips - I will surely be using those to improve my apps :)Arthrospore
C
3

As shown here, you can initialize arbitrarily large amounts of data in the background using a SwingWorker, publish() interim results, and update your view component's model on the EDT in process(). Use a PropertyChangeListener to pace any progress animation. Disable controls that should not be used until loading is complete; enable them when the StateValue is DONE. In this manner,

  • Results will begin appearing immediately.
  • There will be reduced perceived latency.
  • The GUI will remain responsive.

The approach works best with view components that minimize rendering using the flyweight pattern, e.g. JTable, JList, JTextArea, etc. Profile to find the best candidates for re-factoring.

Campbell answered 15/10, 2014 at 15:7 Comment(1)
The problem with this approach for some applications is that they are fragmented a lot or even completely consist of separate plugins loaded in runtime and each plugin might have its own source of data and its own way of loading that data and its own way of publishing that data into application. Though I guess that could be a good approach for each separate plugin and overall application will simply lazy-load its parts one by one when specific part is requested.Arthrospore
B
3

If you're loading data or having to connect to remote services or other lengthy applications, then a SwingWorker is the simpler of the available approches, as it provides progress support via its setProgress method and PropertyChangeListener support.

It provides UI synchronisation via it's process/publish methods, allowing you to safely update the UI and the done method and/or the PropertyChangeListener can be used to determine when the worker is done

If all you're doing is trying to loading UI elements then you should consider a lazy loading approach.

So rather then loading ALL the UI elements at once, especially when the user may not actually look at sections of the UI, it is a much better idea to load those elements only when you absolutely have to and save yourself the time.

When using things like CardLayout or JTabbedPane, this is a much better to only initialise the UI and data of the views when they actually become visible.

If you really want, you could also have it unload when the view is no longer visible, disconnecting listeners from the data source, stopping it form responding to changes to the data source that the user will not actually see...

Birkle answered 15/10, 2014 at 19:45 Comment(5)
That might be a good way - I would just postpone the loading of some UI parts to lazy-load them later, that will reduce the initial loading time and almost never will freeze the UI. I guess that is the way IntelliJ IDEA application is loaded looking at how it initializes on slow machines.Arthrospore
@MikleGarin It's a common enough concept, don't need it, don't load it. We do this alot, but we also do it for menus, as they can change context depending on what the user is doing/done...a complete pain in the ... code. iOS does this ALOT behind the scenes (I imagine the Android does to), which makes sense when you have limited resourcesBirkle
@Birkle that is actually a really good tip about menus - was usually keeping them loaded and "up-to-date" which is indeed a pain code-wise.Arthrospore
@MikleGarin It is a constant balancing act between responsive UI and quick loading, sometimes it works for quick loading, sometimes it works for responsiveness, you need to make judgement calls on each part, the same approach might not work all time ;)Birkle
@Birkle true, the whole case is pretty vague and might require different approaches depending on the application specifics, but still thanks for the useful tips - I will surely be using those to improve my apps :)Arthrospore

© 2022 - 2024 — McMap. All rights reserved.