HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaModerate

Workout tracker

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
workouttrackerstackoverflow

Problem

Around June of last year, I made my very first GUI 'app.' Wish I knew of this amazing site then, I thought for posterity's sake it would be great to get it reviewed.

It's a simple I/O window just to keep track of the quantity of X exercise that was done, and if I used these two other workout apps.

I'd like to evaluate:

  • The good vs bad practices in the code.



  • Any pitfalls I don't account for.



  • General feedback & suggested implementations on all aspects of the code.



`import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class Workout extends JFrame {

public Workout() {
setLayout(new GridLayout(5, 3));
// Labels
add(new JLabel("Workout"));
add(new JLabel("Sets"));
add(new JLabel("Reps"));
}

public static void main(String[] args) throws Exception {
// Get current date information
Calendar cal = Calendar.getInstance();
// Set format to month/day/year hours:minutes
SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yy HH:mm a");

// Creates file object and sets pointer
File file = new File("Workout Log.txt"); // File name + path
// Ensures file exists and is not a directory
if (!file.exists() || file.isDirectory()) {
PrintWriter writer = new PrintWriter("Workout Log.txt", "UTF-8");
writer.close();
}

// Write to file without overwriting current contents
final PrintWriter output = new PrintWriter(new FileWriter(file, true));
output.println("\n" + sdf.format(cal.getTime())); // Add date to file

Solution

Okay, lets begin at the very beginning (since that's a very good place to start. Allegedly).

Swing is the Java GUI framework (now EOL, so probably not worth learning really). As with most GUI frameworks, it's single threaded. It replaces AWT, which was an attempt as a thread safe framework. It was decided, for a number of reasons, that thread safety and GUIs don't mix.

For more information on why exactly, consider reading this excellent article. But for now, lets just take that as read.

So, Swing is single threaded. What does that mean for your code. Well, Swing requires that all interactions with Swing components happen from the Event Dispatch Thread (EDT). You application runs the initialisation and design of the GUI on the application's main thread.

The usual idiom for this is to put your startup logic somewhere else, maybe a GUI class, many an init method and call SwingUtilities.invokeLater. This queues up the initialisation logic on the EDT event queue for later.

I will assume Java 8 is being used for the rest of this review.

Now onto the code. We have 3 distinct processes that the code goes through:

  • startup



  • user input



  • writing output



Currently all these are bundled in the same class, and actually the same method.

Lets begin by extracting a Model, we won't be aiming for a full Model View Presenter pattern here, but merely try to illustrate code separation.

The Model stores the data and contains the business logic for that data. What you have are 3 different workout types, where each workout has a name a reps and a sets. This gives us a starting point.

To avoid confusion lets rename your Workout class to WorkoutApplication.

Now, lets create a Workout class:

public class Workout {

    private String name;
    private int reps;
    private int sets;

    //getters and setters

    //equals and hashCode

    //toString
}


We need to add all the standard Java gumpf: getters and setters, equals and hashCode and toString. I won't cover those here, lets just assume they exist and are correct.

Now, our model has one more role that it needs to perform, it needs to write itself to a file. Since a file is simply a number of lines, each of which is a String, lets add a method to the model to turn itself into a file representation:

public String toFileRepresentation() {
    return String.format("%s x %s %s", sets, reps, name);
}


This simply uses a format string to define how to stick together the details of this particular Workout, for storing in a File.

Now lets move onto our interface Presenter. What does that need to do?

I will ignore the abs & sqat checkboxes because they don't seem to do anything.

The Presenter needs to present inputs for some number of workouts. It then needs to ability to save the current state of those inputs back onto the workouts in question.

Lets define two methods on our interface:

public interface WorkoutPresenter {

    void edit(List workouts);

    List flush();
}


So we can edit a List of workouts onto out presenter, so show them for editing. And we can flush our presenter, to return the List of workouts as it currently stands.

We also, however, need another interface. This we can call a UIHandler interface. This is the interface that the Presenter will use to fire events back to the View. This only needs one method, to be called when log is clicked.

public interface WorkoutUiHandler {
    void doLog();
}


So, now we can implements our WorkoutPresenter. Lets create a WorkoutPresenterImpl class.

```
public class WorkoutPresenterImpl extends JFrame implements WorkoutPresenter {

private final WorkoutUiHandler workoutUiHandler;
private final JPanel workoutPanel = new JPanel();
private WorkoutTableModel workoutTableModel;

public WorkoutPresenterImpl(WorkoutUiHandler workoutUiHandler) {
this.workoutUiHandler = workoutUiHandler;
setLayout(new BorderLayout());
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setTitle("My Workout Tracker");
add(workoutPanel, BorderLayout.CENTER);
final JButton log = new JButton("Log");
log.addActionListener(e -> workoutUiHandler.doLog());
add(log, BorderLayout.SOUTH);
setVisible(true);
pack();
}

@Override
public void edit(List workouts) {
workoutPanel.removeAll();
workoutTableModel = new WorkoutTableModel(workouts);
final JTable jTable = new JTable(workoutTableModel);
final JScrollPane scrollPane = new JScrollPane(jTable);
workoutPanel.add(scrollPane);
pack();
}

@Override
public List flush() {
return workoutTableModel.getWorkouts();
}

private static class WorkoutTableModel extends AbstractTableModel {

private static final int WORKOUT_COL = 0;
private static final int SET_COL = 1;
private static final int REP_COL = 2

Code Snippets

public class Workout {

    private String name;
    private int reps;
    private int sets;

    //getters and setters

    //equals and hashCode

    //toString
}
public String toFileRepresentation() {
    return String.format("%s x %s %s", sets, reps, name);
}
public interface WorkoutPresenter {

    void edit(List<Workout> workouts);

    List<Workout> flush();
}
public interface WorkoutUiHandler {
    void doLog();
}
public class WorkoutPresenterImpl extends JFrame implements WorkoutPresenter {

    private final WorkoutUiHandler workoutUiHandler;
    private final JPanel workoutPanel = new JPanel();
    private WorkoutTableModel workoutTableModel;

    public WorkoutPresenterImpl(WorkoutUiHandler workoutUiHandler) {
        this.workoutUiHandler = workoutUiHandler;
        setLayout(new BorderLayout());
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        setTitle("My Workout Tracker");
        add(workoutPanel, BorderLayout.CENTER);
        final JButton log = new JButton("Log");
        log.addActionListener(e -> workoutUiHandler.doLog());
        add(log, BorderLayout.SOUTH);
        setVisible(true);
        pack();
    }

    @Override
    public void edit(List<Workout> workouts) {
        workoutPanel.removeAll();
        workoutTableModel = new WorkoutTableModel(workouts);
        final JTable jTable = new JTable(workoutTableModel);
        final JScrollPane scrollPane = new JScrollPane(jTable);
        workoutPanel.add(scrollPane);
        pack();
    }

    @Override
    public List<Workout> flush() {
        return workoutTableModel.getWorkouts();
    }

    private static class WorkoutTableModel extends AbstractTableModel {

        private static final int WORKOUT_COL = 0;
        private static final int SET_COL = 1;
        private static final int REP_COL = 2;
        private static final String[] COLUMNS = {"Workout", "Sets", "Reps"};
        private final List<Workout> workouts;

        private WorkoutTableModel(List<Workout> workouts) {
            this.workouts = workouts;
        }

        public List<Workout> getWorkouts() {
            return workouts;
        }

        @Override
        public int getRowCount() {
            return workouts.size();
        }

        @Override
        public int getColumnCount() {
            return COLUMNS.length;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            final Workout workout = workouts.get(rowIndex);
            switch (columnIndex) {
                case WORKOUT_COL:
                    return workout.getName();
                case SET_COL:
                    return workout.getSets();
                case REP_COL:
                    return workout.getReps();
                default:   
                    throw new IllegalArgumentException("Unknown column " + columnIndex);
            }
        }

        @Override
        public String getColumnName(int column) {
            return COLUMNS[column];
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            if (columnIndex == SET_COL || columnIndex == REP_COL) {
                return Integer.class;
            }
            return super.getColumnClass(columnIndex);
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return columnIndex == SET_COL || columnIndex

Context

StackExchange Code Review Q#79196, answer score: 10

Revisions (0)

No revisions yet.