The "simple" answer to your question could be answered by reading (and understanding) Passing Information to a Method or a Constructor however, you seem to be digging yourself into a hole which isn't helping you achieve what you seem to want to achieve.
Important, the following isn't a "answer" per se, but more of a directional change which will, in the long run, help you achieve the things you want.
The "Data"
The first thing you want to do is seperate the "date" from the "interface". The data should drive the UI, not the other way round. You also need to beware, it's the UI's responsibility to "display" the data, so your data should avoid providing "display" information (like toString
).
Lets start with some very basic concepts. You have a recipe, a recipe has a number of measurements of an ingredient.
For convince, recipes could be grouped into "books".
These concepts might look something like this...
public enum Unit {
CUP, MILLI_LETER, EACH, GRAMS, TEA_SPOON
}
public interface Measurement {
public double getQuantity();
public Unit getUnit();
}
public interface Ingredient {
public String getName();
public Measurement getMeasurement();
public static String ALMOND_FLOUR = "Almond Flour";
public static String ALL_PURPOSE_FLOUR = "All Purpose Flour";
public static String UNSALTED_BUTTER = "Unsalted Butter";
public static String BAKING_SODA = "Baking Soda";
public static String BAKING_POWDER = "Baking Powder";
public static String VANILLA_EXTRACT = "Vanilla Extract";
public static String COCOA_POWDER = "Cocoa Powder";
public static String SALT = "Salt";
public static String CHOCOLATE_CHIPS = "Chocolate Chips";
public static String INSTANT_ESPRESSO = "Instant Espresso";
public static String VEGETABLE_OIL = "Vegetable Oil";
public static String BANANAS = "Bananas";
public static String GRANULATED_SUGAR = "Granulated Sugar";
public static String POWDERED_SUGAR = "Powdered Sugar";
public static String LIGHT_BROWN_SUGAR = "Light Brown Sugar";
public static String EGG = "Egg";
public static String EGG_WHITE = "Egg White";
}
public interface Recipe {
public String getName();
public Ingredient[] getIngredients();
}
public interface RecipeBook {
public String getName();
public Recipe[] getRecipes();
}
The use of enum
for the Unit
will restrict the acceptable units. You might find you need more units and you'd expand the concept here.
You should note a unit might be represented differently, for example "liter" and "milli-liters". Personally, I'd avoid having both and instead choice one and then have some kind of converter/formatter which could better render the result, for example 0.5
liters would be displayed as 500 mls
- this is a very important point. The way the data is stored and the way the data is displayed should be independent from each other and it's not the responsibility of the data to dictate presentation.
Now, the above makes use of interfaces
which describe functionality. This supports the Hide Implementation Details principle.
Your code should not care how a Recipe
is implemented, only that it provides the functionality described by the interface. This is often termed as a "contract".
This means that a Recipe
could be build manually, or come from a database or web service, but the code that uses it simply doesn't care.
But, we will need some implementations, so should start with some "default" implementations for our needs...
public class DefaultMeasurement implements Measurement {
private double quantity;
private Unit unit;
public DefaultMeasurement(double quantity, Unit unit) {
this.quantity = quantity;
this.unit = unit;
}
@Override
public double getQuantity() {
return quantity;
}
@Override
public Unit getUnit() {
return unit;
}
}
public class DefaultIngredient implements Ingredient {
private String name;
private Measurement measurement;
public DefaultIngredient(String name, Measurement measurement) {
this.name = name;
this.measurement = measurement;
}
@Override
public String getName() {
return name;
}
@Override
public Measurement getMeasurement() {
return measurement;
}
}
public class DefaultRecipe implements Recipe {
private String name;
private Ingredient[] ingredients;
public DefaultRecipe(String name, Ingredient[] ingredients) {
this.name = name;
this.ingredients = ingredients;
}
@Override
public String getName() {
return name;
}
@Override
public Ingredient[] getIngredients() {
return ingredients;
}
}
public class DefaultRecipeBook implements RecipeBook {
private String name;
private Recipe[] recipes;
public DefaultRecipeBook(String name, Recipe[] recipes) {
this.name = name;
this.recipes = recipes;
}
@Override
public String getName() {
return name;
}
@Override
public Recipe[] getRecipes() {
return recipes;
}
}
These aren't anything special, but do provide us with a starting point.
Up till now, we've not done any UI work. In fact, you could take the above code and make a console app out of them or they could form the bases for web service or web app, they are decoupled, making them, re-usable!
Now, we need to start making some decisions. For example, we could build up some default recipes and books, maybe doing something like...
public class RecipeBuilder {
private String name;
private List<Ingredient> ingredients = new ArrayList<>(8);
public RecipeBuilder(String name) {
this.name = name;
}
public RecipeBuilder with(Ingredient ingredient) {
getIngredients().add(ingredient);
return this;
}
protected String getName() {
return name;
}
protected List<Ingredient> getIngredients() {
return ingredients;
}
public Recipe build() {
List<Ingredient> ingredients = getIngredients();
return new DefaultRecipe(getName(), ingredients.toArray(new Ingredient[ingredients.size()]));
}
}
public class GrandmasRecipeBook extends DefaultRecipeBook {
public GrandmasRecipeBook() {
super("Grandmas",
new Recipe[]{
new RecipeBuilder("Banana Bread")
.with(new DefaultIngredient(Ingredient.BANANAS, new DefaultMeasurement(6, Unit.EACH)))
.with(new DefaultIngredient(Ingredient.UNSALTED_BUTTER, new DefaultMeasurement(30, Unit.GRAMS)))
.with(new DefaultIngredient(Ingredient.GRANULATED_SUGAR, new DefaultMeasurement(0.75, Unit.CUP)))
.with(new DefaultIngredient(Ingredient.ALL_PURPOSE_FLOUR, new DefaultMeasurement(1, Unit.CUP)))
.with(new DefaultIngredient(Ingredient.BAKING_SODA, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.with(new DefaultIngredient(Ingredient.SALT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.with(new DefaultIngredient(Ingredient.VANILLA_EXTRACT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.build()
});
}
}
Here, we've used a builder pattern to make it easier to build up a concept of a Recipe
, again, it's de-coupled in the fact that the underlying implementation of Recipe
is unknown to the caller ... and the caller doesn't care.
I will note, however, this does contradict the Composition Over Inheritance principle, so you need to beware, sometimes doing this can be considered an anti-pattern, so, instead, I'll do something different.
Making it pretty (ie the UI)
Up until till now, we've not touched the UI at all, let's take a look at how that might work...
public class RecipeLibraryPane extends JPanel {
protected enum View {
RECIPE_BOOKS,
RECIPE_BOOK_CONTENTS,
RECIPE,
}
private CardLayout cardLayout;
private RecipesListPane recipesListPane;
private RecipeBookPane recipeBookPane;
private RecipePane recipePane;
public RecipeLibraryPane(RecipeBook[] books) {
cardLayout = new CardLayout();
recipesListPane = new RecipesListPane(
books,
new RecipesListPane.Observer() {
@Override
public void didSelectRecipeBook(RecipesListPane source, RecipeBook book) {
recipeBookPane.setRecipeBook(book);
show(View.RECIPE_BOOK_CONTENTS);
}
});
recipeBookPane = new RecipeBookPane(new RecipeBookPane.Observer() {
@Override
public void didSelectRecipe(RecipeBookPane source, Recipe recipe) {
recipePane.setRecipe(recipe);
show(View.RECIPE);
}
@Override
public void didNavigateBack(RecipeBookPane source) {
show(View.RECIPE_BOOKS);
}
});
recipePane = new RecipePane(
new RecipePane.Observer() {
@Override
public void didNavigateBack(RecipePane source) {
show(View.RECIPE_BOOK_CONTENTS);
}
});
setLayout(cardLayout);
add(recipesListPane, View.RECIPE_BOOKS);
add(recipeBookPane, View.RECIPE_BOOK_CONTENTS);
add(recipePane, View.RECIPE);
show(View.RECIPE_BOOKS);
}
protected void add(Component comp, View view) {
add(comp, view.name());
}
protected void show(View view) {
cardLayout.show(this, view.name());
}
}
This is the primary UI. It manages the navigation between the different views. I've used a enum
(and some convenience methods) to manage the views because I will ALWAYS mistype the view names .
Also note, the list of books is passed to the UI, supporting dependency injection
// Encapsulate some of the re-usable workflows
public class LabelFactory {
public enum Style {
TITLE,
SUB_TITLE,
DEFAULT;
}
private String text;
private Style style;
public LabelFactory(String text) {
this.text = text;
}
protected Style getStyle() {
return style;
}
protected String getText() {
return text;
}
public LabelFactory with(Style style) {
this.style = style;
return this;
}
public JLabel build() {
JLabel label = new JLabel(getText());
Font font = label.getFont();
switch (getStyle()) {
case TITLE:
label.setFont(font.deriveFont(Font.BOLD, font.getSize() * 3));
break;
case SUB_TITLE:
label.setFont(font.deriveFont(Font.BOLD, font.getSize() * 2));
break;
}
return label;
}
}
I found myself wanting to modify some of the text, so I used a "factory" to generate labels with different styles. Because we're relying on the default label font and size, we're actually getting some dynamic support, always nice.
public class RecipesListPane extends JPanel {
public interface Observer {
public void didSelectRecipeBook(RecipesListPane source, RecipeBook book);
}
private JList<RecipeBook> recipesList;
public RecipesListPane(RecipeBook[] recipeBooks, Observer observer) {
recipesList = new JList<>(recipeBooks);
recipesList.setCellRenderer(new RecipeBookListCellRender());
setLayout(new BorderLayout());
add(new LabelFactory("Recipes").with(LabelFactory.Style.TITLE).build(), BorderLayout.NORTH);
add(new JScrollPane(recipesList));
recipesList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
RecipeBook book = recipesList.getSelectedValue();
if (book == null) {
return;
}
observer.didSelectRecipeBook(RecipesListPane.this, book);
recipesList.setSelectedValue(null, false);
}
});
}
public class RecipeBookListCellRender extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value instanceof RecipeBook) {
RecipeBook book = (RecipeBook) value;
value = book.getName();
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
}
This displays the list of recipe books (ie it's a book case!). Please note, I've enclosed the renderer within the class, you might find it more useful to have this class separated into it's own context which would make it more re-usable. But also note, the renderer is been used to make choices about "how" the books are displayed to the user.
For example, you could add support for images at some point in the future and the renderer could then be updated to display them if and when you wanted to.
public class RecipeBookPane extends JPanel {
public interface Observer {
public void didSelectRecipe(RecipeBookPane source, Recipe recipe);
public void didNavigateBack(RecipeBookPane source);
}
private JList<Recipe> recipesList;
private JLabel recipeBookTitleLabel;
public RecipeBookPane(Observer observer) {
recipesList = new JList<>();
recipesList.setCellRenderer(new RecipeListCellRender());
setLayout(new BorderLayout());
JButton backButton = new JButton("<");
backButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
observer.didNavigateBack(RecipeBookPane.this);
}
});
JPanel titlePane = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.anchor = GridBagConstraints.LINE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
recipeBookTitleLabel = new LabelFactory("Book title goes here")
.with(LabelFactory.Style.TITLE)
.build();
titlePane.add(backButton, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
titlePane.add(recipeBookTitleLabel, gbc);
titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);
add(titlePane, BorderLayout.NORTH);
add(new JScrollPane(recipesList));
recipesList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
Recipe recipe = recipesList.getSelectedValue();
if (recipe == null) {
return;
}
observer.didSelectRecipe(RecipeBookPane.this, recipe);
recipesList.setSelectedValue(null, false);
}
});
}
public void setRecipeBook(RecipeBook book) {
// Because this is dynamic, this could have an adverse effect on the
// packed size of the window, just beware
recipeBookTitleLabel.setText(book.getName());
DefaultListModel model = new DefaultListModel<Recipe>();
for (Recipe recipe : book.getRecipes()) {
model.addElement(recipe);
}
recipesList.setModel(model);
}
public class RecipeListCellRender extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value instanceof Recipe) {
Recipe book = (Recipe) value;
value = book.getName();
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
}
This simply displays all the recipes for a given book. Normally I'd use dependency Injection and pass the RecipeBook
via the constructor, but that's not really how CardLayout
works (you could make it work that way, but it becomes messy), so instead, the RecipeBook
is passed via a setter.
public class RecipePane extends JPanel {
public interface Observer {
public void didNavigateBack(RecipePane source);
}
private JList<Recipe> inredientsList;
private JLabel recipeTitleLabel;
public RecipePane(Observer observer) {
inredientsList = new JList<>();
inredientsList.setCellRenderer(new IngredientListCellRender());
setLayout(new BorderLayout());
JButton backButton = new JButton("<");
backButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
observer.didNavigateBack(RecipePane.this);
}
});
JPanel titlePane = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.anchor = GridBagConstraints.LINE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
recipeTitleLabel = new LabelFactory("Recipe title goes here")
.with(LabelFactory.Style.TITLE)
.build();
titlePane.add(backButton, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
titlePane.add(recipeTitleLabel, gbc);
titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);
add(titlePane, BorderLayout.NORTH);
add(new JScrollPane(inredientsList));
inredientsList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
}
});
}
public void setRecipe(Recipe recipe) {
// Because this is dynamic, this could have an adverse effect on the
// packed size of the window, just beware
recipeTitleLabel.setText(recipe.getName());
DefaultListModel model = new DefaultListModel<Recipe>();
for (Ingredient ingredient : recipe.getIngredients()) {
model.addElement(ingredient);
}
inredientsList.setModel(model);
}
public class IngredientListCellRender extends DefaultListCellRenderer {
private NumberFormat formatter;
public IngredientListCellRender() {
formatter = NumberFormat.getNumberInstance();
formatter.setMinimumFractionDigits(0);
}
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value instanceof Ingredient) {
// You could, very easily, decouple the formatting of the
// ingredients and units to there own format implementations,
// allowing a greater level of flexibility in how
// certain elements are formatted. You could then use
// dependency injection to pass the formatter(s) into this
// class, allowing for a greater level of customisation
Ingredient ingredient = (Ingredient) value;
Measurement measurement = ingredient.getMeasurement();
StringBuilder sb = new StringBuilder(64);
sb.append(formatter.format(measurement.getQuantity()))
.append(" ");
switch (measurement.getUnit()) {
case CUP:
sb.append(" Cups ");
break;
case GRAMS:
sb.append(" Grams ");
break;
case MILLI_LETER:
sb.append(" mls ");
break;
case TEA_SPOON:
sb.append(" Tea spoons ");
break;
}
sb.append(ingredient.getName());
value = sb.toString();
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
}
And finally, the recipe itself, displaying the ingredients.
There's a repeating pattern here, can you see it? The "back" button implementation is been repeated!
While not serious, it is something that could come back to haunt us if we choice to change the navigation workflow, so, instead, lets try and seperate it.
public class NavigationPane extends JPanel {
public interface Observer {
public void didNavigateBack(NavigationPane source);
}
private JButton backButton;
private Observer observer;
public NavigationPane(JPanel content, Observer observer) {
this.observer = observer;
setLayout(new BorderLayout());
JPanel navigationPane = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.anchor = GridBagConstraints.LINE_START;
navigationPane.add(getBackButton(), gbc);
add(navigationPane, BorderLayout.NORTH);
add(content);
}
public JButton getBackButton() {
if (backButton != null) {
return backButton;
}
backButton = new JButton("<");
backButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
navigateBack();
}
});
return backButton;
}
public Observer getObserver() {
return observer;
}
protected void navigateBack() {
Observer observer = getObserver();
if (observer == null) {
return;
}
observer.didNavigateBack(this);
}
}
This is a pretty simple wrapper class which simply wraps the navigation UI around a pre-existing UI element.
We can start by changing the RecipePane
to remove the navigation workflow...
public class RecipePane extends JPanel {
//...
public RecipePane() {
inredientsList = new JList<>();
inredientsList.setCellRenderer(new IngredientListCellRender());
setLayout(new BorderLayout());
JPanel titlePane = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.anchor = GridBagConstraints.LINE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
recipeTitleLabel = new LabelFactory("Recipe title goes here")
.with(LabelFactory.Style.TITLE)
.build();
gbc.fill = GridBagConstraints.HORIZONTAL;
titlePane.add(recipeTitleLabel, gbc);
titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);
add(titlePane, BorderLayout.NORTH);
add(new JScrollPane(inredientsList));
inredientsList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
// Do you want to do something here?!
}
});
}
//...
}
And update the RecipeLibraryPane
to support it...
public class RecipeLibraryPane extends JPanel {
//...
public RecipeLibraryPane(RecipeBook[] books) {
//...
recipePane = new RecipePane();
//...
add(new NavigationPane(recipePane, new NavigationPane.Observer() {
@Override
public void didNavigateBack(NavigationPane source) {
show(View.RECIPE_BOOK_CONTENTS);
}
}), View.RECIPE);
//...
}
}
And now we're better supporting the Single Responsibility Principle (as the navigation UI is been managed by a seperate class independent from the other class, which really didn't care about navigation)
(nb: I've not changed RecipeBookPane
to support this, I'll leave that to you)
And finally, how do we actually launch this UI? Maybe by place something like...
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
RecipeBook[] books = new RecipeBook[]{
new DefaultRecipeBook(
"Grandmas",
new Recipe[]{
new RecipeBuilder("Banana Bread")
.with(new DefaultIngredient(Ingredient.BANANAS, new DefaultMeasurement(6, Unit.EACH)))
.with(new DefaultIngredient(Ingredient.UNSALTED_BUTTER, new DefaultMeasurement(30, Unit.GRAMS)))
.with(new DefaultIngredient(Ingredient.GRANULATED_SUGAR, new DefaultMeasurement(0.75, Unit.CUP)))
.with(new DefaultIngredient(Ingredient.ALL_PURPOSE_FLOUR, new DefaultMeasurement(1, Unit.CUP)))
.with(new DefaultIngredient(Ingredient.BAKING_SODA, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.with(new DefaultIngredient(Ingredient.SALT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.with(new DefaultIngredient(Ingredient.VANILLA_EXTRACT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
.build()
})
};
JFrame frame = new JFrame();
frame.add(new RecipeLibraryPane(books));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
into your main
method.
You should also take the time to have a read of: