Page tree
Skip to end of metadata
Go to start of metadata

In situations where a content app doesn't fit the use case it might be necessary to create a custom app. When creating a custom app you have a few options to consider. You can go completely custom where everything within the shell is custom using Vaadin. It is also possible to introduce custom Vaadin widgets to be used by your app. Another approach is to use EmbeddedPageSubApp which allows you to embed a UI using an iframe. Finally, there is SmallAppLayout which works well for editing single things such as mail configuration. Combined with the form builder, it allows you to configure a form-based UI. This approach is minimal and focused, reducing complexity even beyond content apps. In this blog we'll take a look at how to create apps using SmallAppLayout. We'll take a look at all the needed pieces to implement a small app.

Understanding MVP

Magnolia uses the model-view-presenter design pattern. The separation of views and presenters is done consistently throughout the application on server and client and within complex components. The presenter reacts on events raised by user interaction, driving the view by populating it with data to display. The pattern can be summed up with the following statements.

  • The View does not implement business logic or communicate with the Model.
  • The View and Presenter communicate together.
  • The Presenter uses the Model to fetch data and implements business logic.
  • The Model is an interface defining the data to be displayed or otherwise acted upon.

From a graphical perspective that would look like this:

Here we also see the event bus pictured. The Presenter can dispatch events within the system using one of four different buses. The different buses correspond to different scopes within the system.

SmallAppLayout

There are many examples of the SmallAppLayout in Magnolia so using it will make your small app feel like a native app. This type of layout is offering space for a description and multiple sections stacked vertically. The sections can take in any Object which implements the interface com.vaadin.ui.Component. The sections have a max width of 900px.

Calculator

Take the example of a basic calculator. A basic calculator has a face which contains digits 0-9 and functions add, subtract, multiply, divide, equals, and clear. The face of calculator is equivalent to the View and provides the user a way to interact and perform desired calculations.

BasicCalculator.java
public interface Calculator extends View {
     
    void setListener(Listener listener);
    void updateDisplay(String displayValue);
 
    public interface Listener {
        void keyPressed(String key);
    }
}

The View provides a way for the Presenter to register itself as the Listener of events. This way the View knows who to contact about UI events. It also provides a way to update it's display. The internals of how the display is updated will be handled in implementation class of the Calculator interface. The View does not know what to display, only how to display it, so updateDisplay() takes in the parameter displayValue.

We also have the inner listener interface to be implemented by the Presenter. In simple terms, the Presenter will be the Calculator's listener, and will handle the event of a key pressed. In this particular design we have chosen to use a single method for all keys. We will let Presenter sort out which key it was and what to do about it. If necessary, the Presenter can then call updateDisplay() and pass in the value to be displayed.

The Presenter only knows about the View's Interface. The Presenter implements the behavior/business logic and and informs the View to display something. When a UI event occurs, the View informs the Presenter keyPressed(), the Presenter updates the View updateDisplay().

Processor.java
public class Processor implements Calculator.Listener {
      
    private Calculator view;
    private String displayValue;
      
    @Inject
    public Processor(Calculator view) {
        this.view = view;
        view.setListener(this);
    }
      
    @Override
    public void keyPressed(String key) {
         
        // Numeric key pressed 
        if (Character.isDigit(key.charAt(0))) {
            ...
            ...
        }
         
        // Function key pressed 
        else {
            ...
            ...
        }
         
        // Inform the View on what to display 
        view.updateDisplay(this.displayValue);
    }
}

Implementing the Calculator

Since we have pushed the key recognition logic to the Presenter it will necessary for the Presenter to provide a method for supported keys. That method will be getCpations() which returns a two dimensional array of button captions. Also Calculator will provide a build() method for building its UI.

BasicCalculator.java
public interface Calculator extends View {
    
    void setListener(Listener listener);
    void updateDisplay(String displayValue);
    void build();

    public interface Listener {
        void keyPressed(String string);
        String[][] getCaptions();
    }
}

The Processor will implement both methods of the inner Listener interface. The View will be injected into the Processor constructor at runtime. The Processor will provide a start() method to set itself as the Listener for the View, to build() the View, and return the View to the caller.

Processor.java
package info.magnolia.training.calculator.app;
 
import javax.inject.Inject;
 
public class Processor implements Calculator.Listener {
     
    private Calculator view;
    private String displayValue = "0";
    private Function function = null;
    private double register = 0;
     
    private enum Function { ADD, SUBTRACT, MULTIPLY, DIVIDE, EQUALS }
     
    public String[][] getCaptions() {
        return new String[][]
            {{ "7", "8", "9", "/" },
             { "4", "5", "6", "x" },
             { "1", "2", "3", "-" },
             { "0", ".", "=", "+" }};
    }
     
    @Inject
    public Processor(Calculator view) {
        this.view = view;
    }
     
    public Calculator start() {
        view.setListener(this);
        view.build();
        return view;
    }
     
    @Override
    public void keyPressed(String key) {
         
        // Numeric key pressed
        if (Character.isDigit(key.charAt(0)) || key.charAt(0) == '.')
            this.displayValue = "0".equals(this.displayValue) ? key : this.displayValue + key;
         
        else // Function key pressed
            switch (this.getFunction(key)) {
                case EQUALS:
                    this.displayValue = Double.toString(this.calculate());
                    this.register = 0;
                    this.function = null;
                    break;
                default:
                    this.register = Double.parseDouble(this.displayValue);
                    this.displayValue = "0";
                    this.function = this.getFunction(key);
                    return; // don't update display
            }
         
        view.updateDisplay(this.displayValue);
    }
     
    protected final Calculator getView() {
        return this.view;
    }
     
    protected final String getDisplayValue() {
        return this.displayValue;
    }
     
    private Function getFunction(String key) {
        switch (key.charAt(0)) {
            case '+': return Function.ADD;
            case '-': return Function.SUBTRACT;
            case 'x': return Function.MULTIPLY;
            case '/': return Function.DIVIDE;
            case '=': return Function.EQUALS;
            default : return null;
        }
    }
 
    private double calculate() {
        switch (this.function) {
            case ADD:      this.register += Double.parseDouble(this.displayValue); break;
            case SUBTRACT: this.register -= Double.parseDouble(this.displayValue); break;
            case MULTIPLY: this.register *= Double.parseDouble(this.displayValue); break;
            case DIVIDE:   this.register /= Double.parseDouble(this.displayValue); break;
            default:       return this.register;
        }
         
        return this.register;
    }
}

Implementing the Calculator will consist or extending SmallAppLayout and adding sections to it. In this case we will use one section which is the Calculator face.

CalculatorImpl.java
public class CalculatorImpl extends SmallAppLayout implements Calculator {

    private final TextField display = new TextField();
    private Calculator.Listener listener;

	private final static int BUTTON_SIZE = 35;    

    public void build() {
        final String[][] captions = listener.getCaptions();
        VerticalLayout section = new VerticalLayout();
        
        Button button;
        
        this.display.setWidth(Integer.toString(BUTTON_SIZE * captions.length) + "px");
        this.display.setValue("0");
        section.addComponent(new HorizontalLayout(this.display));
        
        HorizontalLayout row;
        for (int i = 0; i < captions.length; i++) {
            row = new HorizontalLayout();
            for (int j = 0; j < captions[i].length; j++) {
                button = new Button(captions[i][j]);
                button.addClickListener(new Button.ClickListener() {
                    @Override
                    public void buttonClick(Button.ClickEvent event) {
                        listener.keyPressed(event.getButton().getCaption());
                    }
                });
                button.setWidth(Integer.toString(BUTTON_SIZE) + "px");
                row.addComponent(button);
            }
            section.addComponent(row);
        }
        addSection(section);
    }

    @Override
    public Component asVaadinComponent() {
        return this;
    }

    @Override
    public void updateDisplay(String displayValue) {
        this.display.setValue(displayValue);
    }

    @Override
    public void setListener(Calculator.Listener listener) {
        this.listener = listener;
    }
}

CalculatorSubApp

The final piece is the subApp class which we use the register the app in Magnolia. The Presenter will be injected into the CalculatorSubApp constructor at runtime. This class will call the start() method of the Presenter.

BasicCalculatorSubApp.java
public class CalculatorSubApp extends BaseSubApp<Calculator> {
    
    @Inject
    protected CalculatorSubApp(SubAppContext subAppContext, Processor presenter) {
        super(subAppContext, presenter.start());
    }
}


Register for Injection

As mentioned, the View will be injected into the Processor and the Presenter will be injected into the CalculatorSubApp. So we must register the implementations for both the Calculator and the Processor. Along with it we also register the CalculatorSubApp. All of this is done in the module descriptor file.

calculator-app.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module SYSTEM "module.dtd" >
<module>
  <name>calculator-app</name>

  <class>info.magnolia.module.calculator.CalculatorAppModule</class>
  <versionHandler>info.magnolia.module.calculator.setup.CalculatorAppModuleVersionHandler</versionHandler>
  <version>${project.version}</version>

  <components>
    <id>app-calculator</id>
  </components>
  <components>
    <id>app-calculator-main</id>
    <component>
      <type>info.magnolia.module.calculator.app.CalculatorSubApp</type>
      <implementation>info.magnolia.module.calculator.app.CalculatorSubApp</implementation>
    </component>
    <component>
      <type>info.magnolia.module.calculator.view.Calculator</type>
      <implementation>info.magnolia.module.calculator.view.CalculatorImpl</implementation>
    </component>
    <component>
      <type>info.magnolia.module.calculator.presenter.Processor</type>
      <implementation>info.magnolia.module.calculator.basic.Processor</implementation>
    </component>
  </components>
  
  <dependencies>
    <dependency>
      <name>core</name>
      <version>5.5/*</version>
    </dependency>
  </dependencies>
</module>

 

Calculate

As a final configuration register the app in the UI then it's ready for use.


  • No labels