You can effect dependency injection into controllers using a controller factory. So in short, if you have some kind of model class, you can use a controller factory to pass values to the controller's constuctor:
Model model = ... ;
Callback<Class<?>, Object> controllerFactory = type -> {
try {
for (Constructor<?> c : type.getConstructors()) {
if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == BookingModel.class) {
return c.newInstance(model);
}
}
// no appropriate constructor: just use default:
return type.newInstance();
} catch (Exception exc) {
throw new RuntimeException(exc);
}
};
FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/fxml"));
loader.setControllerFactory(controllerFactory);
Parent view = loader.load();
So in the case you describe, you would create one model for your "A" flow, create the controller factory from that model, and then use that controller factory when you load A1, A2, and A3. Then create another model instance, a controller factory from that second model instance, and use that controller factory to load B1, B2, and B3.
To make this more concrete, consider a hotel room booking application, which we can divide into three parts (for demonstration purposes): setting the arrival date, setting the departure date, and confirming the booking. The three pieces each need to access the same data, which would be kept in a model class. We can also use that model class to maintain the current state of the flow; e.g. which of the three booking steps (arrival, departure, confirm) we are in. Something like:
public class BookingModel {
private final ObjectProperty<LocalDate> arrival = new SimpleObjectProperty<>();
private final ObjectProperty<LocalDate> departure = new SimpleObjectProperty<>();
private final BooleanProperty confirmed = new SimpleBooleanProperty();
private final ObjectProperty<Screen> screen = new SimpleObjectProperty<>();
public enum Screen {
ARRIVAL, DEPARTURE, CONFIRMATION
}
public BookingModel() {
arrival.addListener((obs, oldArrival, newArrival) -> {
if (departure.get() == null || departure.get().equals(arrival.get()) || departure.get().isBefore(arrival.get())) {
departure.set(arrival.get().plusDays(1));
}
});
}
// set/get/property methods for each property...
}
Each step has an FXML and a controller, and each controller needs access to the model instance shared by the steps in the same flow. So we can do:
public class ArrivalController {
private final BookingModel model ;
@FXML
private DatePicker arrivalPicker ;
@FXML
private Button nextButton ;
public ArrivalController(BookingModel model) {
this.model = model ;
}
public void initialize() {
arrivalPicker.valueProperty().bindBidirectional(model.arrivalProperty());
arrivalPicker.disableProperty().bind(model.confirmedProperty());
nextButton.disableProperty().bind(model.arrivalProperty().isNull());
}
@FXML
private void goToDeparture() {
model.setScreen(BookingModel.Screen.DEPARTURE);
}
}
and
public class DepartureController {
private final BookingModel model ;
@FXML
private DatePicker departurePicker ;
@FXML
private Label arrivalLabel ;
@FXML
private Button nextButton ;
public DepartureController(BookingModel model) {
this.model = model ;
}
public void initialize() {
model.setDeparture(null);
departurePicker.setDayCellFactory(/* cell only enabled if date is after arrival ... */);
departurePicker.valueProperty().bindBidirectional(model.departureProperty());
departurePicker.disableProperty().bind(model.confirmedProperty());
arrivalLabel.textProperty().bind(model.arrivalProperty().asString("Arrival date: %s"));
nextButton.disableProperty().bind(model.departureProperty().isNull());
}
@FXML
private void goToArrival() {
model.setScreen(BookingModel.Screen.ARRIVAL);
}
@FXML
private void goToConfirmation() {
model.setScreen(BookingModel.Screen.CONFIRMATION);
}
}
and
public class ConfirmationController {
private final BookingModel model ;
@FXML
private Button confirmButton ;
@FXML
private Label arrivalLabel ;
@FXML
private Label departureLabel ;
public ConfirmationController(BookingModel model) {
this.model = model ;
}
public void initialize() {
confirmButton.textProperty().bind(Bindings
.when(model.confirmedProperty())
.then("Cancel")
.otherwise("Confirm"));
arrivalLabel.textProperty().bind(model.arrivalProperty().asString("Arrival: %s"));
departureLabel.textProperty().bind(model.departureProperty().asString("Departure: %s"));
}
@FXML
private void confirmOrCancel() {
model.setConfirmed(! model.isConfirmed());
}
@FXML
private void goToDeparture() {
model.setScreen(Screen.DEPARTURE);
}
}
Now we can create a "booking flow" with
private Parent createBookingFlow() {
BookingModel model = new BookingModel() ;
model.setScreen(Screen.ARRIVAL);
ControllerFactory controllerFactory = new ControllerFactory(model);
BorderPane flow = new BorderPane();
Node arrivalScreen = load("arrival/Arrival.fxml", controllerFactory);
Node departureScreen = load("departure/Departure.fxml", controllerFactory);
Node confirmationScreen = load("confirmation/Confirmation.fxml", controllerFactory);
flow.centerProperty().bind(Bindings.createObjectBinding(() -> {
switch (model.getScreen()) {
case ARRIVAL: return arrivalScreen ;
case DEPARTURE: return departureScreen ;
case CONFIRMATION: return confirmationScreen ;
default: return null ;
}
}, model.screenProperty()));
return flow ;
}
private Node load(String resource, ControllerFactory controllerFactory) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource(resource));
loader.setControllerFactory(controllerFactory);
return loader.load() ;
} catch (IOException exc) {
throw new UncheckedIOException(exc);
}
}
with the ControllerFactory
defined following the pattern at the beginning of the answer:
public class ControllerFactory implements Callback<Class<?>, Object> {
private final BookingModel model ;
public ControllerFactory(BookingModel model) {
this.model = model ;
}
@Override
public Object call(Class<?> type) {
try {
for (Constructor<?> c : type.getConstructors()) {
if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == BookingModel.class) {
return c.newInstance(model);
}
}
// no appropriate constructor: just use default:
return type.newInstance();
} catch (Exception exc) {
throw new RuntimeException(exc);
}
}
}
and this will work if we need multiple "flows":
public class BookingApplication extends Application {
@Override
public void start(Stage primaryStage) {
SplitPane split = new SplitPane();
split.getItems().addAll(createBookingFlow(), createBookingFlow());
split.setOrientation(Orientation.VERTICAL);
Scene scene = new Scene(split, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Parent createBookingFlow() {
// see above...
}
private Node load(String resource, ControllerFactory controllerFactory) {
// see above...
}
public static void main(String[] args) {
launch(args);
}
}
Complete example as a gist.
It's not clear to me how to easily set this up using a Dependency Injection framework such as Spring. The issue is controlling the granularity of the creation of the booking model: you don't want it to have singleton scope (because different flows would need different models), but you don't want prototype scope either (because different controllers within the same flow need the same model). In a sense you would need something akin to "session" scope, though a session here is not a HttpSession
but would be a custom session tied to the "flow". As far as I know there is no way in Spring to generalize the definition of a session; though others with more Spring expertise may have a way, and users of other DI frameworks may know if this is possible in those frameworks.