Home > Software engineering >  Wait for long-running operation and show popup
Wait for long-running operation and show popup

Time:11-19

Is ist possible to wait for a method (say METHOD1) to finish, but if it is running longer than X secs, call another method until METHOD1 returns?

Some pseudocode:

method1();
startCountdown(1000); // time in millis
while (method1() still running) {
    method2(); // shows a popup with spinner (Swing/AWT)
}

I guess, it must be done with concurrency, but I am not used to concurrent programming. So I have no idea how to start...

The UI framework used is Swing/AWT.

CodePudding user response:

So, the basic idea would be to use a combination of a SwingWorker and a Swing Timer.

The idea is if the Timer triggers before the SwingWorker is DONE, you execute some other workflow, otherwise you stop the Timer, for example...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JLabel label;
        private JButton startButton;

        boolean hasCompleted = false;

        public TestPane() {
            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;

            label = new JLabel("Waiting for you");
            startButton = new JButton("Start");

            add(label, gbc);
            add(startButton, gbc);

            startButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    startButton.setEnabled(false);
                    startWork();
                }
            });
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        protected void startWork() {
            label.setText("Something wicked this way comes");

            // You could build an isoloated workflow, which allowed you to pass
            // three targets, the thing to be executed, the thing to be 
            // executed if time run over and the thing to be executed when
            // the task completed (all via a single interface),
            // but, you get the idea
            Timer timer = new Timer(2000, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (hasCompleted) {
                        return;
                    }
                    label.setText("Wickedness is a bit slow today");
                }
            });
            timer.setRepeats(false);

            SomeLongRunningOperation worker = new SomeLongRunningOperation();
            worker.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    switch (worker.getState()) {
                        case DONE:
                            hasCompleted = true;
                            timer.stop();
                            label.setText("All is done");
                            startButton.setEnabled(true);
                            break;
                    }
                }
            });
            worker.execute();
            timer.start();
        }

    }

    public class SomeLongRunningOperation extends SwingWorker<Void, Void> {

        @Override
        protected Void doInBackground() throws Exception {
            Thread.sleep(5000);
            return null;
        }

    }
}

Play around with the timings to see what different effects you get.

Why use a SwingWorker? Because it has it's own state callbacks, which makes it easier to deal with

As I said in my comments, you could distill the workflow down into a re-usable concept, something like...

public class TimedTask<V> {
    
    public static interface Task<V> {
        public V execute() throws Exception;
    }
    
    public static interface TimedTaskListener<V> extends EventListener {
        public void taskIsTakingLongThenExepected(TimedTask task);
        public void taskDidComplete(TimedTask task, V value);
    }
    
    private Task<V> task;
    private TimedTaskListener<V> listener;
    
    private V value;
    
    private int timeOut;
    private Timer timer;
    private SwingWorker<V, Void> worker;
    private boolean hasCompleted = false;

    public TimedTask(int timeOut, Task<V> task, TimedTaskListener<V> listener) {
        this.task = task;
        this.listener = listener;
        this.timeOut = timeOut;
    }

    public V getValue() {
        return value;
    }

    public int getTimeOut() {
        return timeOut;
    }

    protected Task<V> getTask() {
        return task;
    }

    protected TimedTaskListener<V> getListener() {
        return listener;
    }
    
    public void execute() {
        if (timer != null || worker != null) {
            return;
        }
        
        hasCompleted = false;
        worker = new SwingWorker<V, Void>() {
            @Override
            protected V doInBackground() throws Exception {
                value = task.execute();
                return value;
            }
        };
        worker.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                switch (worker.getState()) {
                    case DONE:
                        hasCompleted = true;
                        timer.stop();
                        getListener().taskDidComplete(TimedTask.this, value);
                        break;
                }
            }
        });
        
        timer = new Timer(getTimeOut(), new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (hasCompleted) {
                    return;
                }
                getListener().taskIsTakingLongThenExepected(TimedTask.this);
            }
        });
        timer.setRepeats(false);

        worker.execute();
        timer.start();
    }
    
}

And then you could replace the startWork method in the first example with something like...

protected void startWork() {
    label.setText("Something wicked this way comes");
    TimedTask.Task<Void> task = new TimedTask.Task<Void>() {
        @Override
        public Void execute() throws Exception {
            Thread.sleep(5000);
            return null;
        }
    };
    TimedTask<Void> timedTask = new TimedTask(2000, task, new TimedTask.TimedTaskListener<Void>() {
        @Override
        public void taskIsTakingLongThenExepected(TimedTask task) {
            label.setText("Wickedness is taking it's sweet time");
        }

        @Override
        public void taskDidComplete(TimedTask task, Void value) {
            label.setText("Wickedness has arrived");
            startButton.setEnabled(true);
        }
    });
    timedTask.execute();
}

CodePudding user response:

I've struggled with this question before. What I ended up doing was, creating a separate class that extends AsyncTask. Added an interface/listener to this class that returned my object. Right before I start my AsyncTask, I'll disable buttons and put up a loading spinner. Once the AsyncTask comes back, I'll do my processing and reenable the buttons and take down the loading spinner. Of coarse I'm doing a rest call in the example, but it can be applied to anything that takes awhile. The reason why this is a better option than a while loop is that it's won't be burning cycles checking conditions.

public class RestCall extends AsyncTask {
    private Context mContext;
    private static final String TAG = "RestCall";
    private AsyncResponse mListener;

 public RestCall(Context context, URL url, AsyncResponse listener) {
        this.mListener = listener;
        this.mContext = context;
        this.url = url;
    }

 public interface AsyncResponse {
        void processFinish(JSONArray results);
    }

@Override
protected Object doInBackground(Object[] objects) {
    Log.d(TAG, "doInBackground: Thread: "   Thread.currentThread().getName());

    return getResultsInJSONArray(url);

}

private JSONArray getResultsInJSONArray(URL url) {
//Here is where you will be doing the bulk of the work
//Doing a rest call and
//Processing results to JSONArray
}

@Override
protected void onPostExecute(Object o) {
    super.onPostExecute(o);
    Log.d(TAG, "onPostExecute: Handing off Object");
    mListener.processFinish((JSONArray) o);
}

Now in your original class you'll add the following to your class:

public class myClass

private restCall call;

Than create a listener from that interface you made. Then pass the results to a method.

restCall.AsyncResponse listener = results -> handleResults(results); 

With the listener setup you can you can execute your AsyncTask.

//here is were you would throw up the loading bar.
call = new restCall(this, url, listener);
call.execute();

 private void handleResults(JSONArray results){
//process what you need to
//take down loading bar
}
  • Related