MVP - ThorbenKuck/Keller GitHub Wiki
The MVP-Package allows you to use predefined Classes, to organize MVP-Applications.
The premiss of this are 3 core interfaces: View
, Presenter
and ViewController
. It allows you, to use any Window-API that is provided with java.
Following is an example for JavaFX.
The first Class we want to take a look at, is the MainApplication. It is just used, to start the Application.
public class MainApplication extends Application {
private final ViewController controller = new AsynchronousViewController(this::runOnFXThread);
public static void main(String[] args) {
launch(args);
}
private void runOnFXThread(Runnable runnable) {
if(Platform.isFxApplicationThread()) {
runnable.run();
} else {
Platform.runLater(runnable);
}
}
@Override
public void start(final Stage primaryStage) throws Exception {
// the load method is described later in this example
load();
}
}
What we want to achieve here, is that we create a MainStage, consisting of the ExampleView, which is explained later. The ExampleView is just an Implementation of the View. We then can tell our ViewController to open any main view (window) and do not have to keep track over open windows and stuff.
Before we take a look at our ExampleView,lets first define another class, that we need for our ExampleView, the CustomStage
public class CustomStage extends Stage implements Show { }
This class implements the "Show" interface, which is used for opening and closing such views. The View
interface inherits from this. Our CustomStage is used a (sort of) proxy for opening Stages. Since the javafx Stage already has show
and close
defined, we need to do no work at all here.
So, now lets look at our ExampleView:
public class ExampleView extends CustomStage implements View<ExamplePresenter> {
private ExamplePresenter presenter;
public ExampleView(final ExamplePresenter presenter) {
this.presenter = presenter;
}
@Override
public ExamplePresenter getPresenter() {
return presenter;
}
@Override
public void instantiate() throws InstantiationException {
final Pane parent = new Pane();
Scene scene = new Scene(parent);
setScene(scene);
setWidth(ThreadLocalRandom.current().nextInt(200, 600));
setHeight(ThreadLocalRandom.current().nextInt(200, 600));
}
}
Here we see the "instantiate" idea. In instantiate we could now load our .fxml file and throw an InstantiationException, if we could not load it. Also, we hold an refference to to Presenter-interface for this view. It is important to note, that we take the interface, not the concrete instance for that in our constructor. We want to be able to interchange the view and presenter, without touching the other one.
Here, you should have interactions defined in an custom interface (like IExamplePresenter), which defines what methods the ExamplePresenter has and inject this into the constructor. Then you also would have to change the generic type to ... implements View<IExamplePresenter>
.
This would be way better, as you now may change the Implementation of this IExamplePresenter, without needing to change the ExampleView (The same holds true for the Presenter later, but i am getting ahead of my self).
I simply use the instance of that presenter (which is a not so good idea), but it is just used as an example.
We set the width and height each to a random value between 200 and 600 (mathematically speaking: [200, 600]), so we later see the difference.
For our presenter, it looks like this:
public class ExamplePresenter implements Presenter<ExampleView> {
private ExampleView view;
@Override
public void onClose() {
System.out.println("sadly saying goodbye...");
}
@Override
public void setView(final ExampleView exampleView) {
this.view = exampleView;
}
@Override
public ExampleView getView() {
return view;
}
}
Here we see, how we get the cyclic dependency between our view and presenter. The Method setView
is called by an default method, named instantiate
, which calls instantiate on the view and than the setView method.
Again, it would be wise to abstract the ExampleView through an interface, like IExampleView and inject his instead. This would allow us, to change the implementaion of this ExampleView (to now maybe use Swing or something like this), without the need to change this Presenter. Again, i am not doing this, so that this example is kept simple and easyer to understand.
Now, this is an simple presenter. You would inject any Model into the Constructor here. The presenter is like a "proxy" between the view and model (which might be anything), to allow simple interchangability between different UI-APIs or models without touching the other end.
To allow our MainApplication to work, we have to do 2 more thing. Give it a constructor and the load method. Its constructor like this:
public JavaFXStageControllerExample() {
controller.addFactory(ExampleView.class, () -> {
ExamplePresenter examplePresenter = new ExamplePresenter();
ExampleView exampleView = new ExampleView(examplePresenter);
examplePresenter.instantiate(exampleView);
return exampleView;
});
}
This is the basic idea, to provide a factory, that creates a view and conntects it to the Presenter, which in and of it self is created. You could now go further and cache it or something, but let's keep it simple for the moment. Here you would inject models into the Presenter.
This default constructor is called, if we call launch(args)
. So we inject our simple example Factory to our controller. In our last step, lets actually create it. For that, lets write our load method:
private void load() {
controller.openMainStage(ExampleView.class);
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(() -> {
try {
controller.openSeparateStage(ExampleView.class).synchronize();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1, TimeUnit.SECONDS);
}
In the first line of this method, we create our main stage as an ExampleView (which is only a blank window).
Then we schedule every second, to open the ExampleView as a new, seperate Window. This means, it will not override the default window, but actually be maintained seperatly. Since we can have only one of each window as a seperate window, the "old" instance from 1 second ago, will be closed every time we call openSeparateStage
.
If you were to assamble the whole code and start it, you would see a window beeing opened. After 1 second a new windows opens. After another 1 second, this window closes and a new windows is created. So on and so forth.
So, whenever you would open a new window you would inject this instance of the ViewController
.
Just a side-node: You should inject this ViewController only into an Presenter. Never into a view. But this should be common sense, if you code with MVP