patternjavaModerate
Workout tracker
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:
`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
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
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:
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
To avoid confusion lets rename your
Now, lets create a
We need to add all the standard Java gumpf: getters and setters,
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
This simply uses a format string to define how to stick together the details of this particular
Now lets move onto our
I will ignore the
The
Lets define two methods on our interface:
So we can
We also, however, need another
So, now we can
```
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
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 || columnIndexContext
StackExchange Code Review Q#79196, answer score: 10
Revisions (0)
No revisions yet.