io.github.dinamo541.corefx.navigation

FlowController

Singleton final thread-safe JavaFX

The heart of the library. A single, lazily-initialized controller that loads and caches FXML views, builds and swaps scenes, opens independent windows and modal dialogs (blocking and non-blocking), replaces regions of containers and BorderPanes, manages full-screen and minimum size, and carries internationalization plus a typed value passed between controllers. El corazón de la librería. Un único controlador, inicializado de forma perezosa, que carga y cachea vistas FXML, construye e intercambia escenas, abre ventanas independientes y diálogos modales (bloqueantes y no bloqueantes), reemplaza regiones de contenedores y BorderPanes, gestiona pantalla completa y tamaño mínimo, y transporta internacionalización más un valor tipado que se pasa entre controladores.

OverviewVisión general

Almost every JavaFX app writes the same navigation glue: build an FXMLLoader, call load(), grab the controller, wrap the root in a Scene, set it on a Stage. FlowController does this once. You initialize it with where your views and resources live, then navigate by view name — the loader is created, cached and reused, so the controller for any view stays reachable. Casi toda app JavaFX escribe el mismo pegamento de navegación: construir un FXMLLoader, llamar a load(), obtener el controlador, envolver la raíz en una Scene y asignarla a un Stage. FlowController hace esto una sola vez. Lo inicializas indicando dónde viven tus vistas y recursos, y luego navegas por nombre de vista — el loader se crea, cachea y reutiliza, así que el controlador de cualquier vista permanece accesible.

🧠
Initialization-on-demand singletonSingleton de inicialización bajo demanda

Obtain the instance with FlowController.getInstance(). The instance is created lazily and thread-safely by the JVM class loader — no synchronization cost on the hot path.Obtén la instancia con FlowController.getInstance(). La instancia se crea de forma perezosa y thread-safe por el cargador de clases de la JVM — sin coste de sincronización en la ruta caliente.

The one rule: initialize firstLa única regla: inicializa primero

You must call one initialize(...) overload exactly once before any navigation or loading method. Every public method guards this with an internal checkInitialized() and throws IllegalStateException if you forget. Calling initialize twice also throws — the configuration is locked in with double-checked locking. Debes llamar a una sobrecarga de initialize(...) exactamente una vez antes de cualquier método de navegación o carga. Cada método público lo protege con un checkInitialized() interno y lanza IllegalStateException si lo olvidas. Llamar a initialize dos veces también lanza excepción — la configuración queda bloqueada mediante double-checked locking.

Quick startInicio rápido

public class MyApp extends Application {
    @Override
    public void start(Stage stage) {
        FlowController flow = FlowController.getInstance();
        flow.initialize(
            stage,
            "My Application",
            "/com/example/views/",      // .fxml live here
            "/com/example/resources/",  // other resources
            "/com/example/icon.png",    // window icon
            MyApp.class                 // resolves classpath resources
        );
        flow.goViewMain("Home");        // loads Home.fxml and shows the stage
    }
}

From any controller afterwards, navigate without touching FXMLLoader again:Después, desde cualquier controlador, navega sin volver a tocar FXMLLoader:

FlowController flow = FlowController.getInstance();

flow.goViewMain("Dashboard");         // swap the main scene's root
flow.goViewInModal("EditUser");       // open a non-blocking modal
flow.goViewInModalAndWait("Confirm"); // open a blocking modal, wait for close

Initialization overloadsSobrecargas de inicialización

Six overloads progressively fill in defaults so simple apps stay terse while complex ones keep full control. They all delegate to the core six-argument method. Seis sobrecargas van completando valores por defecto para que las apps simples queden concisas mientras las complejas conservan el control total. Todas delegan en el método central de seis argumentos.

void initialize(Class<?> appClass)

Creates a fresh Stage for you and derives the app name and base paths from the package of appClass. The leanest startup.Crea un Stage nuevo por ti y deriva el nombre de la app y las rutas base del paquete de appClass. El arranque más escueto.

void initialize(Stage stage, Class<?> appClass)

Uses your stage; derives name (last package segment) and paths from appClass.Usa tu stage; deriva el nombre (último segmento del paquete) y las rutas de appClass.

void initialize(Stage stage, String appName, Class<?> appClass)

Custom window title; base path derived from the package.Título de ventana personalizado; ruta base derivada del paquete.

void initialize(Stage stage, String appName, String basePath, Class<?> appClass)

Views resolve to basePath + "view/", resources to basePath + "resources/", and the icon to resources/appName.png.Las vistas se resuelven en basePath + "view/", los recursos en basePath + "resources/" y el icono en resources/appName.png.

void initialize(Stage, String appName, String baseViewPath, String baseResourcePath, String appIconPath, Class<?> appClass)

The core method. Every String argument must be non-blank; stage and appClass must be non-null. Sets the window title and locks the configuration.El método central. Cada argumento String no puede estar en blanco; stage y appClass no pueden ser null. Fija el título de la ventana y bloquea la configuración.

void initialize(… , Consumer<Scene> themeApplier[, ResourceBundle idioma])

Full setup plus a theme applier and (optionally) an i18n bundle. Both are registered only after core init succeeds, so a failed init never leaves partial state. Pass null for either to skip it.Configuración completa más un aplicador de temas y (opcionalmente) un bundle de i18n. Ambos se registran solo después de que la init central tenga éxito, así una init fallida nunca deja estado parcial. Pasa null en cualquiera para omitirlo.

🎨
Recommendation — wire theming at initRecomendación — conecta el theming en la init

Pair this with ThemeManager: pass themes.asApplier() as the themeApplier so every scene the controller builds is themed and tracked automatically.Combínalo con ThemeManager: pasa themes.asApplier() como el themeApplier para que cada escena que construya el controlador quede con tema y rastreada automáticamente.

FXML loader managementGestión de loaders FXML

Loaders are cached per view name in a ConcurrentHashMap, so a view's controller stays reachable across navigations. Los loaders se cachean por nombre de vista en un ConcurrentHashMap, así el controlador de una vista permanece accesible entre navegaciones.

MethodMétodoPurposePropósito
getLoader(String)Returns the cached loader, creating & caching it on a miss.Devuelve el loader cacheado, creándolo y cacheándolo si no existe.
createLoaderInstance(String)Always builds a fresh, already-load()ed loader (bypasses the cache). Throws IOException if the FXML is missing/unparsable.Siempre construye un loader nuevo, ya con load() ejecutado (ignora la caché). Lanza IOException si el FXML falta o no se puede parsear.
<T> getController(String)Convenience: the controller of a cached view, cast to T.Conveniencia: el controlador de una vista cacheada, casteado a T.
removeLoader(String)Evicts one view so it reloads from disk next time.Expulsa una vista para que se recargue desde disco la próxima vez.
clearLoadersMap()Evicts all views.Expulsa todas las vistas.
// Reach a freshly-loaded view's controller to pass it data:
HomeController c = flow.getController("Home");
c.setUser(currentUser);
⚠️
Cached controllers keep stateLos controladores cacheados conservan estado

Because loaders are cached, a view's controller is reused across visits — its fields are not reset for you. If a screen must start clean every time, call removeLoader(name) before navigating, or reset state in the controller.Como los loaders se cachean, el controlador de una vista se reutiliza entre visitas — sus campos no se reinician por ti. Si una pantalla debe empezar limpia cada vez, llama a removeLoader(name) antes de navegar, o reinicia el estado en el controlador.

void goViewMain(String viewName)

The workhorse. Builds a scene on first use (wrapping the root in an anchored AnchorPane and applying the theme), or swaps the existing scene's root, then shows the stage if hidden. Throws IllegalArgumentException for a blank name.El caballo de batalla. Construye una escena en el primer uso (envolviendo la raíz en un AnchorPane anclado y aplicando el tema), o intercambia la raíz de la escena existente, y luego muestra el stage si estaba oculto. Lanza IllegalArgumentException ante un nombre en blanco.

void changeViewInMain(String viewName)

Like goViewMain but replaces the root inside its current container when possible, falling back to scene.setRoot.Como goViewMain pero reemplaza la raíz dentro de su contenedor actual cuando es posible, recurriendo a scene.setRoot si no lo es.

Windows & modalsVentanas y modales

Independent windows don't block; modals block their owner; and-wait modals block the calling code until dismissed.Las ventanas independientes no bloquean; los modales bloquean a su owner; los modales and-wait bloquean el código llamante hasta que se cierran.

MethodMétodoBehaviourComportamiento
goViewInWindow(name[, resizable])New top-level, independent window. Closing it doesn't affect the main stage.Nueva ventana de nivel superior e independiente. Cerrarla no afecta al stage principal.
goViewInModal(name[, owner][, resizable])WINDOW_MODAL dialog, centered, blocks its owner.Diálogo WINDOW_MODAL, centrado, bloquea a su owner.
goViewInModalAndWait(name[, owner][, resizable])Same, but uses showAndWait() — returns only after the dialog closes.Igual, pero usa showAndWait() — retorna solo después de que el diálogo se cierra.
// Confirmation flow: open, wait, then read the result the dialog left behind.
flow.setTransferValue(invoice);
flow.goViewInModalAndWait("ConfirmDelete");
Boolean confirmed = flow.getTransferValue(Boolean.class);
🧵
FX thread onlySolo en el hilo de FX

goViewInModalAndWait uses showAndWait() and must run on the JavaFX Application Thread. Calling it off-thread throws. On hidden, modals/windows swap in an empty Pane so the cached view detaches cleanly.goViewInModalAndWait usa showAndWait() y debe ejecutarse en el JavaFX Application Thread. Llamarlo fuera de ese hilo lanza excepción. Al ocultarse, los modales/ventanas colocan un Pane vacío para que la vista cacheada se desacople limpiamente.

Containers & BorderPane regionsContenedores y regiones de BorderPane

For shell layouts (sidebar + content), swap only a region instead of the whole scene. Containers may be a Pane or Group; unsupported types fall back to a setter you provide. Para layouts tipo shell (barra lateral + contenido), intercambia solo una región en vez de la escena completa. Los contenedores pueden ser un Pane o un Group; los tipos no soportados recurren a un setter que tú proporcionas.

// Swap the center region of the shell's BorderPane:
flow.changeViewInBorderPane("Reports");                 // defaults to "Center"
flow.changeViewInBorderPane("Menu", shell, "Left");     // a specific region
flow.clearRegion("Right", shell);                        // empty a region

Valid BorderPane regions are "Center", "Top", "Bottom", "Left", "Right" (a blank region defaults to "Center"; anything else throws IllegalArgumentException).Las regiones válidas de BorderPane son "Center", "Top", "Bottom", "Left", "Right" (una región en blanco usa "Center" por defecto; cualquier otra lanza IllegalArgumentException).

InternationalizationInternacionalización

void setIdioma(ResourceBundle idioma)

Registers the bundle passed to every future loader, so FXML %key references resolve. Because cached loaders capture the bundle at creation, this clears the loader cache — which is exactly what makes runtime language switching work. Pass null to disable localization.Registra el bundle que se pasa a todo loader futuro, de modo que las referencias %key de FXML se resuelvan. Como los loaders cacheados capturan el bundle al crearse, esto limpia la caché de loaders — que es justo lo que permite cambiar de idioma en tiempo de ejecución. Pasa null para desactivar la localización.

ResourceBundle getIdioma()

The active bundle, or null.El bundle activo, o null.

flow.setIdioma(ResourceBundle.getBundle("i18n.messages", Locale.forLanguageTag("es")));
// ...later, switch language at runtime (cache is cleared for you):
flow.setIdioma(ResourceBundle.getBundle("i18n.messages", Locale.ENGLISH));
flow.goViewMain("Home"); // re-rendered in the new language

Typed data transferTransferencia de datos tipada

Hand a value to the next controller without the two referencing each other.Entrega un valor al siguiente controlador sin que ambos se referencien entre sí.

MethodMétodoReturns / throwsDevuelve / lanza
setTransferValue(Object)Stores (or, with null, clears) the pending value.Almacena (o, con null, limpia) el valor pendiente.
getTransferValue()The value as an opaque Object (not consumed by reading).El valor como un Object opaco (leerlo no lo consume).
<T> getTransferValue(Class<T>)Cast to T; null if unset; throws IllegalStateException on a type mismatch and NullPointerException if type is null.Casteado a T; null si no está asignado; lanza IllegalStateException ante un tipo incompatible y NullPointerException si type es null.
💡
Transfer value vs. AppContextValor de transferencia vs. AppContext

Use the transfer value for a one-hop hand-off to the very next view. For state many screens read (the logged-in user, a selection), prefer AppContext.Usa el valor de transferencia para una entrega de un solo salto a la vista inmediatamente siguiente. Para estado que muchas pantallas leen (el usuario conectado, una selección), prefiere AppContext.

Stage configuration & accessorsConfiguración del stage y accesores

MethodMétodoPurposePropósito
setStageMinSize(w, h) / (stage, w, h)Minimum window size (null stage → main stage).Tamaño mínimo de ventana (stage null → stage principal).
toggleFullScreen() / (stage)Flip full-screen on the main or a given stage.Alterna pantalla completa en el stage principal o uno dado.
setFullScreen(stage, boolean)Set full-screen explicitly.Establece pantalla completa explícitamente.
setThemeApplier(Consumer<Scene>)Callback applied to every scene the controller builds (CSS/theming hook).Callback aplicado a cada escena que construye el controlador (hook de CSS/theming).
getMainStage()The primary stage (throws if not initialized).El stage principal (lanza excepción si no está inicializado).
isInitialized()true once initialize has run.true una vez que initialize se ha ejecutado.
closeMainStage()Closes the main stage (usually exits the app).Cierra el stage principal (normalmente termina la app).

RecommendationsRecomendaciones

Do initialize once in Application.start, then navigate everywhere by view name.Haz la inicialización una vez en Application.start y luego navega en todas partes por nombre de vista.

Do use getController(name) to push data into a screen right before showing it.Usa getController(name) para inyectar datos en una pantalla justo antes de mostrarla.

Avoid calling navigation methods off the FX thread; modals especially require it.Evita llamar a métodos de navegación fuera del hilo de FX; los modales en especial lo exigen.

Remember cached controllers retain state — evict with removeLoader when a screen must be pristine.Recuerda que los controladores cacheados conservan estado — expúlsalos con removeLoader cuando una pantalla deba estar impecable.