Skip to content

Final builder methods #3

Open
Open

Description

@crschnick

I started creating subclasses for various builders and found it limiting that all builder methods, e.g. disable() or visible(), are final. Sometimes there's a point of overriding them for a specific type of builder. So it would be nice if those methods were not final

Activity

  1. hjohn commented on Jan 24, 2026

    @hjohn
    Member

    I'm a bit hesitant here to change this. I find it very important that users can know what to expect, and changing the meaning by allowing overrides can easily break that.

    Also I'm usually rather strict about inheritance and public / protected members; they should either be final, abstract or empty. This is to avoid entangling the super and sub class implementations (ie. if the super class relies on things in an overridable method, then a subclass can break its invariants). So, I'd be willing to add "hook" methods (ie. empty / abstract) but I'm unsure what you are trying to achieve.

    The "leaf" builder types are all final (as you can't extend these properly due to lack of generics) so making those non-final would not really help. What I can do here perhaps to help you a bit is to make another type in between the leaf builder that still has the correct generics (and includes all the methods). So the hierarchy would be like ButtonBuilder (empty mostly, no generic parameters) -> AbstractExtendibleButtonBuilder (contains all the button specific stuff that wasn't in button base, generic) -> AbstractButtonBaseBuilder (the base button stuff according to JavaFX ButtonBase type)

    So if you can explain what you are trying to do then perhaps I can think along.

    I think there maybe some overlap with what I'm working on currently, allowing your to customize (some) builder methods with a strategy. For example, it would allow you to change the meaning of all "text" providing methods (like text, promptText, accessibleText) by having them delegate to a user provided TextStrategy. For example, to do localization you could then do this:

    FXDefaults defaults = FXDefaults.DEFAULTS.textStrategy((text, node, setter) -> {
        // Example: uppercase text for German locale, but you can do resource look-ups
        if (LOCALE.equals(Locale.GERMAN)) {
            setter.accept(text.toUpperCase());
        }
        else {
            setter.accept(text);
        }
    });
    
    // Apply defaults to a pane
    Panes.vbox("root").defaults(defaults).nodes(
        FX.label().text("hello"),
        FX.textField().promptText("enter.name")
    );

    So at any level, you can have a different set of defaults (if desired) which will inherit defaults from ancestor nodes (or the scene) or use a global default if not overridden.

    The FXDefaults class will contain several strategies, is immutable (but you can derive new ones easily) and the strategies are applied at the moment the Node is attached to the Scene. Strategies inherit, so for a specific piece of UI you could override them. Or you could never override them and use global defaults (just set these once at the start of your application before building any UI's). The override is there more for cases where you do want to do something different for a different scene, but I suspect a lot of apps will just set global strategies once.

    This can even be fully dynamic (having your app instantly change all labels to suit a different locale) by registering a listener on something as part of the strategy you provide.

  2. crschnick commented on Jan 30, 2026

    @crschnick
    Author

    The strategies look interesting, I am looking forward to trying that out. You can also do snapshot releases without having to get the full javadocs etc ready to gather some feedback on how it handles.

    In my specific case, I was trying to make various common builders method like visible() to be thread-safe, meaning that I could set the property from any thread without it causing issues

  3. hjohn commented on Jan 30, 2026

    @hjohn
    Member

    The strategies look interesting, I am looking forward to trying that out. You can also do snapshot releases without having to get the full javadocs etc ready to gather some feedback on how it handles.

    I'll look into this, but none of this is even committed (I suppose I could make a branch but it is still very much in flux)... hoping to get something committed this weekend as I think I may have found a nice solution that ticks all the boxes I wanted.

    I've been experimenting with these a lot... have tried like 5 different solutions so far, none of which I really liked (some required scene listeners to resolve strategies just in time, which I think is too heavy handed).

    However, I think I'm close to landing on something now (different from the previous version I showed). This new variant doesn't require any last-minute execution of strategies, but is still flexible enough to override strategies at any point in your Node tree. Here are two sample strategies (names subject to change):

    ImmutableContext context = ImmutableContext.DEFAULT
      .textStrategy((node, inputString) -> {
        Broadcasts.addHandler(node, LocaleChangedEvent.LOCALE_CHANGED, e -> {
          if(e.locale().equals(Locale.GERMAN)) {
            node.setText(inputString.toUpperCase());
          }
          else {
            node.setText(inputString);
          }
        });
      })
      .childrenStrategy((ctx, node, children) -> {
        node.getChildren().setAll(children.stream()
          .filter(Objects::nonNull)
          .map(n -> switch(n) {
            case String s when s.equals("Hi") -> FX.label().text("Bye").build(ctx);
            case String s -> FX.label().text(s.toUpperCase()).build(ctx);
            default -> NodeBuilder.toNode(ctx, n);
          })
          .toList()
        );
      });
    

    The first one will listen for LocaleChangedEvents and modify all texts to a different locale (you can also do this with a static locale, without the Broadcast listener stuff I'm experimenting with if you don't mind restarting/reopening windows).

    The second one allows you to see everything that you added to say Panes.hbox().nodes( .... ) -- so if you added nodes(1, "A", "B", 3) then that's what you get. This enables you to decide for yourself if a plain String should be converted to a label or something else (it was hard coded before).

    The way it works is that you can provide this context object on build methods. Normally you'd probably only want that at Scene level so something like:

    FX.scene().root(  ...build UI here ...).build(context);
    

    But at any point you can also provide a context to any node you want to override something in the middle somewhere by calling its build method. I'm still seeing if this has to be done through the build method (as that finalizes everything) or if this could be delayed and just have a simpler context() method (that would require a field though, currently no extra field is needed).

    Also, if you think you only need a single context for your entire application, there will be a way to override the "default" context once at the start, and then you won't have to provide a context anywhere.

    In my specific case, I was trying to make various common builders method like visible() to be thread-safe, meaning that I could set the property from any thread without it causing issues

    The builder is however gone after you construct the node, and has no knowledge of the node that it last constructed. Calling visible on it again won't do anything.

    If I wanted to build something like this, I'd probably make a small utility to provide thread-safe variants of properties, something like this:

     public final ObservableValue<Boolean> textVisible = ThreadSafeProperties.bool();
    
     // then later:
    
     FX.label().visible(textVisible);
    

    Now you can manipulate the label's visibility from any thread.

  4. crschnick commented on Jan 30, 2026

    @crschnick
    Author

    Yeah the utility approach for the thread-safe properties is also possible, but I always had the issue of sometimes forgetting to do that. And in some cases, modifications from the wrong thread only cause issues rarely. So it's not immediately detected

    I thought it would be easier to just centralize it for my builders so I can't do it wrong

  5. hjohn commented on Jan 30, 2026

    @hjohn
    Member

    Yeah the utility approach for the thread-safe properties is also possible, but I always had the issue of sometimes forgetting to do that. And in some cases, modifications from the wrong thread only cause issues rarely. So it's not immediately detected

    I thought it would be easier to just centralize it for my builders so I can't do it wrong

    Perhaps when you call a method that accepts a property, I can also call a strategy (bindingStrategy), and you can put a wrapper around the property about to be bound or something. I think there are many possibilities, so I'm eager to get something worked out for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions