2. How an Application Is Built¶
A small Swing program fits into one class: a JFrame with a text area, a menu, and the file handling in the menu's action listeners. Many first programs are written that way. It works until you want to test the saving, add a second window, run something in the background or find a bug in a 1,500-line class.
The amadeOS programs share one structure that avoids this. This chapter explains it with the text editor Pad as the example. You will meet the same packages in every application of this book, in every game and in the desktop's own tools.
The repositories¶
| Repository | Contains | Depends on |
|---|---|---|
amade-commons |
The shared library of all amadeOS programs: start-up, CDE look and themes, texts in five languages, settings directories, desktop integration | the JDK, Guice |
amade-<app> |
One application each: amade-pad, amade-mail, amade-writepad, … |
amade-commons, and libraries of its own (a mail library for Mail, a decoder for Audio) |
Dependencies point one way only: an application uses amade-commons, amade-commons never knows a particular application. That is what makes it a library.
The packages of an application¶
All code of the editor is under eu.amadeos.apps.pad:
| Package | Job | Classes in Pad |
|---|---|---|
bootstrap |
Start-up and wiring | AmadePadApplication, PadModule |
domain |
What the program is about, no user interface | TextDocument, TextFormat, DocumentState |
repository |
Files on disk | DocumentRepository, FileSystemDocumentRepository, TextCodec |
service |
Work that is neither rules nor storage | DocumentService, SearchService |
ui.main |
The main window and its presenter | MainView, MainPresenter, MainWindow, MainMenuBar |
ui.editor, ui.search |
Components and dialogs | EditorTab, LineNumberMargin, FindReplaceDialog |
Each package has one job:
domain: what the program is about¶
For a text editor, that is a document: its text, how it is stored on disk, whether it has unsaved changes. For a mail program it is accounts, folders and messages; for a calendar, appointments and their repetitions.
The domain knows nothing about windows. It does not import java.awt or javax.swing. Its classes are often records: values that never change once made. They can be tested with plain JUnit tests that run in milliseconds and need no screen.
repository: files and storage¶
Documents, settings and caches are read and written here, and nowhere else. If a file format changes, only this package changes. The rest of the program sees an interface like DocumentRepository with read and write, and never a byte.
service: the work in between¶
Searching a text, fetching mail, calculating a fractal, talking to a scanner. Services often run in the background and return their result later (more on that below). Like the domain, they never touch Swing.
ui: what the user sees¶
Everything with Swing lives under ui. In ui.main the main window is assembled, and the presenter decides what to show. That is the next topic.
bootstrap: putting it together¶
The main method and the wiring of all parts. More on that below.
Model-View-Presenter¶
How do the documents and the window talk to each other? Pad uses Model-View-Presenter (MVP), like all amadeOS applications:
flowchart LR
W["<b>View</b><br/>MainWindow<br/><i>Swing: shows, forwards</i>"]
P["<b>Presenter</b><br/>MainPresenter<br/><i>decides, no Swing</i>"]
M["<b>Model and services</b><br/>DocumentState, DocumentService<br/><i>documents and files</i>"]
W -- "user chose File → Open" --> P
P -- "open(path)" --> M
M -- "the document, or an error" --> P
P -- "addEditor(), load(...), showError(...)" --> W
- The model is the domain, together with the services that work on it.
- The view is the window. It shows what it is told and reports what the user does. It decides nothing.
- The presenter sits between them. It receives the user's actions, asks the services and tells the view what to show. It is plain Java, without Swing.
The key is that the presenter does not know the window class. It only knows an interface, which lists exactly what the presenter needs from a view:
package eu.amadeos.apps.pad.ui.main;
import eu.amadeos.apps.pad.ui.editor.EditorView;
import java.nio.file.Path;
import java.util.List;
public interface MainView {
EditorView activeEditor();
List<EditorView> editors();
EditorView addEditor();
void selectEditor(EditorView editor);
void removeEditor(EditorView editor);
Path chooseOpenPath();
Path chooseSavePath(Path currentPath);
SaveChoice confirmClose(EditorView editor);
void showError(String message);
void showInfo(String message);
/** Asks whether a document that its charset cannot store should be saved as UTF-8. */
boolean confirmSaveAsUtf8(String charsetName);
void showSearch(boolean replace);
void findNext();
void showAbout();
void dispose();
enum SaveChoice {SAVE, DISCARD, CANCEL}
}
Notice what the interface does not say: nothing about JFileChooser, JOptionPane or tabs. "Ask for a file to open" is one method, chooseOpenPath(); how the question looks is the window's business. A test can answer it with a fixed path, and the whole open-edit-save cycle runs without a screen. Chapter 10 shows such a test.
Tip: a rule of thumb
If you are about to write an if in the window, stop and ask whether it is a decision. Decisions belong in the presenter or the model, where tests can reach them.
MVP is not free. Pad needs a view interface with fifteen methods, and in the window a few lines that forward each menu choice to the presenter. For a program of a few hundred lines this feels like ceremony. It pays back as soon as you change something and the tests tell you within seconds that the program still works.
Two threads¶
A desktop program has a problem a command-line program does not have: while it works, the window must stay alive. If Pad read a 50 MB log file on the thread that paints the window, the window would freeze until the file was read.
Swing has one thread for everything visible, the event dispatch thread (EDT). Mouse clicks, key presses, menu actions and painting all run there, one after the other. Two rules follow:
- Every access to a Swing component happens on the EDT. Swing is not thread-safe; touching a component from another thread causes rare, random errors that are hard to find.
- Nothing slow happens on the EDT. Files, network, long calculations go to another thread.
amadeOS applications do slow work on virtual threads, the light threads Java has had since version 21: thousands of them cost less than ten ordinary ones. The service starts the work and returns a CompletableFuture, a promise of a result. The presenter tells the future to continue on the EDT, with an Executor from amade-commons that runs a task there:
EDT virtual thread
─── ──────────────
presenter.open(path)
documents.open(path) ─────────────▶ read the bytes, decode them
(EDT is free: painting, typing) ...
◀───────────── TextDocument
.handleAsync(..., edt)
editor.load(path, document)
Data that crosses between the threads is immutable: a record like TextDocument, which neither side can change under the other's feet. Chapter 10 builds this step by step.
Wiring it together: Guice¶
The presenter needs a view, the document service, the background executor, the EDT executor, the texts and the language settings. The window needs the search service and the texts. Someone has to create all these objects in the right order and hand each one what it needs. That someone is Guice, a small dependency injection library.
You describe once which class to use for what, in a module:
package eu.amadeos.apps.pad.bootstrap;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.Scopes;
import eu.amadeos.commons.i18n.Messages;
import eu.amadeos.apps.pad.repository.DocumentRepository;
import eu.amadeos.apps.pad.repository.FileSystemDocumentRepository;
import eu.amadeos.apps.pad.ui.main.MainView;
import eu.amadeos.apps.pad.ui.main.MainWindow;
/**
* Text editor bindings. The background executor, theme and the other shared services come from
* {@link eu.amadeos.commons.app.CommonsModule}.
*/
public final class PadModule extends AbstractModule {
/** The texts, {@code messages.properties} and its translations. */
public static final String MESSAGES = "eu.amadeos.apps.pad.messages";
@Override
protected void configure() {
bind(DocumentRepository.class).to(FileSystemDocumentRepository.class);
bind(MainWindow.class).in(Scopes.SINGLETON);
bind(MainView.class).to(MainWindow.class);
}
/** The texts in the language {@code AmadeApplication} chose. */
@Provides
@Singleton
Messages messages(java.util.Locale locale) {
return Messages.load(PadModule.class, MESSAGES, locale);
}
}
bind(DocumentRepository.class).to(FileSystemDocumentRepository.class): whoever needs aDocumentRepositorygets the one that works on the file system. A test can bind another.bind(MainView.class).to(MainWindow.class)together within(Scopes.SINGLETON): there is one window, and whoever needs a view gets that one.@Providesmethods build objects that need a little code, like the texts in the right language.- Classes whose constructor is marked
@Inject(the presenter, the window, the services) are built by Guice, which fills in the constructor's parameters.
The main method then only asks for the finished objects:
package eu.amadeos.apps.pad.bootstrap;
import eu.amadeos.commons.app.AmadeApplication;
import eu.amadeos.commons.app.AppInfo;
import eu.amadeos.apps.pad.ui.main.MainPresenter;
import eu.amadeos.apps.pad.ui.main.MainWindow;
import java.nio.file.Path;
public final class AmadePadApplication {
public static final AppInfo APP = new AppInfo("amade-pad", "Text Editor", "pad");
private AmadePadApplication() {
}
public static void main(String[] args) {
AmadeApplication.launch(APP, injector -> {
MainWindow window = injector.getInstance(MainWindow.class);
MainPresenter presenter = injector.getInstance(MainPresenter.class);
window.bind(presenter);
if (args.length == 0) presenter.newDocument();
else for (String arg : args) presenter.open(Path.of(arg));
window.setVisible(true);
}, new PadModule());
}
}
AmadeApplication.launch comes from amade-commons. It does what every amadeOS program needs before its first window: it picks the language, installs the CDE look in the current colour theme and follows the theme when the user changes it, switches to the EDT and creates the injector. Besides the application's own module it adds the CommonsModule, which provides the shared objects:
@Override
protected void configure() {
bind(AppInfo.class).toInstance(app);
bind(XdgDirectories.class).toInstance(xdg);
bind(AppDirectories.class).toInstance(AppDirectories.of(xdg, app.configName()));
bind(ExecutorService.class).toInstance(executor);
bind(EdtExecutor.class).toInstance(new EdtExecutor());
bind(Executor.class).annotatedWith(Ui.class).to(EdtExecutor.class);
bind(ThemeStore.class).toInstance(themes);
bind(DesktopLauncher.class).toInstance(new DesktopLauncher());
bind(PasswordStore.class).toInstance(new SecretToolPasswordStore(app.id()));
bind(LocaleSettings.class).toInstance(locales);
bind(LocaleStore.class).toInstance(locales.desktop());
bind(Locale.class).toInstance(locales.locale());
bind(ContactDirectory.class).toInstance(ContactDirectory.of(xdg));
}
That is where the presenter's ExecutorService (the virtual threads) and EdtExecutor come from, without Pad saying a word about them.
Texts and languages¶
No visible text is written in Java code. Texts live in messages.properties (English) and its translations messages_de.properties, _fr, _es and _it, read through the Messages class of amade-commons:
menu.file=&File
menu.file.new=New
menu.file.open=Open...
menu.file.save=Save
menu.file.save-as=Save As...
menu.file.close-tab=Close Tab
menu.file.exit=Exit
The & marks the mnemonic, the underlined letter that opens a menu with the keyboard (Alt+F). Each language chooses its own letter: &Datei in German, &Fichier in French. Chapter 10 shows how the texts are loaded, formatted and tested.
Summary¶
- Every application is its own repository with the same packages:
bootstrap,domain,repository,service,ui. - The domain and the services never touch Swing, so plain tests cover them.
- Model-View-Presenter: the presenter decides, the view shows, and a test can stand in for the view.
- Two threads: the EDT for everything visible, virtual threads for everything slow; immutable values cross between them.
- Guice wires the parts;
AmadeApplicationstarts the program in the desktop's language and theme.
Now you know the map. In the next chapter we walk it: we build Pad from the document up.