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.
- Completely custom: Everything within the shell is custom using Vaadin. It is also possible to introduce custom Vaadin widgets to be used by your app.
- EmbeddedPageSubApp: Embed a UI using an iframe.
- SmallAppLayout: For editing single things such as mail configuration. Combined with the form builder, it allows you to configure a form-based UI.
In this tutorial we'll take a look at how to create apps using SmallAppLayout and all the needed pieces to implement the 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.
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()
.
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 getCaptions()
which returns a two dimensional array of button captions. Also Calculator
will provide a build()
method for building its UI.
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.
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.
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.
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.
<?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.