Gauging interest in bindings that can delay changing their value (debounce/throttle)

Sam Howman s.howman.4580 at gmail.com
Tue Mar 28 05:17:13 UTC 2023


I love this idea and would use it immediately!

On Fri, Mar 24, 2023 at 12:10 AM John Hendrikx <john.hendrikx at gmail.com>
wrote:

> Hi list,
>
> I've been working on a potential new API (and proof of concept
> implementation) for adding a new type of fluent binding which can delay
> changing their values, and I'm wondering how much interest there is in
> such a thing.
>
> The main purpose of such an API is to prevent being flooded with changes
> when properties change often, or to simply delay certain actions until
> the user has settled on a selection or has stopped typing.
>
> For this purpose I would like to introduce a default method on
> `ObservableValue` with the signature:
>
>      ObservableValue<T> throttle(Throttler throttler);
>
> The parameter `Throttler` can be obtained via static methods of a helper
> class named `FXThrottlers`. These provide various pre-configured
> throttlers that work correctly with JavaFX's event thread model.  My
> current proof of concept provides:
>
>      public static Throttler debounce(Duration quietPeriod);
>      public static Throttler debounceTrailing(Duration quietPeriod);
>      public static Throttler throttle(Duration period);
>      public static Throttler throttleTrailing(Duration period);
>
> These are variations of similar concepts, and vary mostly in when
> exactly they will allow value changes; debouncers will wait for a period
> without any changes, while throttlers will periodically allow changes.
> The trailing variants will not immediately emit the first change but
> will wait for the period to elapse first; all variants will eventually
> take on the value of the source observable.  Debouncing is typically
> used when you wish for an input to settle before taking action (like
> typing in a search bar), while throttling is used to give regular
> feedback but avoid doing so too often (like feedback during window
> resizing).
>
> Usage example which updates a preview panel when the user has finished
> (cursor) scrolling through a list view:
>
>      ObjectProperty<T> selectedItem =
> listView.getSelectionModel().selectedItemProperty();
>
>      selectedItem
> .throttle(FXThrottlers.debounceTrailing(Duration.ofMillis(500)))
>          .addListener((obs, old, current) -> {
>               if (current != null) {
>                   updatePreviewPanel(current);
>               }
>          });
>
> Implementation details:
>
> ObservableValue is part of javafx.base, and as such can't use animations
> or call Platform::runLater.  The ThrottledBinding implementation has
> abstracted all of these out into the Throttler class, and FXThrottlers
> (which would live in javafx.graphics) therefore provides the necessary
> call backs to integrate property changes correctly back onto the JavaFX
> event thread.  The Throttler class also simplifies testing; the test can
> provide its own timing source and background scheduler.  The Throttler
> interface has the following methods:
>
>      /**
>       * Schedules a command to run on an unspecified thread after the time
>       * given by {@code nanos} elapses.
>       *
>       * @param command a command to run, cannot be {@code null}
>       * @param nanos a time in nanoseconds
>       */
>      void schedule(Runnable command, long nanos);
>
>      /**
>       * Provides the current time in nanoseconds.
>       *
>       * @return the current time in nanoseconds
>       */
>      long nanoTime();
>
>      /**
>       * Runs the given command as soon as possible on a thread specified
> by this
>       * throttler for updating property values.
>       *
>       * @param command a command to run, cannot be {@code null}
>       */
>      void update(Runnable command);
>
>      /**
>       * Given the current elapsed time in the current change window, and
> the
>       * amount of time elapsed since the last change was detected,
> determines
>       * if and by how much the current change window should be extended.
>       *
>       * @param elapsed nanoseconds elapsed since the start of the
> current change window
>       * @param elapsedSinceLastChange nanoseconds elapsed since the last
> change
>       * @return nanoseconds to extend the window with
>       */
>      long determineInterval(long elapsed, long elapsedSinceLastChange);
>
> For testing purposes, the schedule and nanoTime can be provided such
> that the throttle function can be tested deterministically. For
> integrating with JavaFX, update is implemented as
> `Platform.runLater(command)`.  The schedule and nanoTime methods
> delegate to an Executor and System.nanoTime respectively.  When using
> properties without JavaFX, Throttler implementations can be provided
> which run property updates on a scheduler thread (just calling
> Runnable::run on the current thread) or via some user provided executor.
>
> A sample test case looks like this (read with a mono space font :-)):
>
>      @Test
>      void testThrottleLeadingAndTrailing() {
>        // create Throttler with deterministic behavior:
>        Throttler throttler =
> create(Throttler.IntervalHandler.throttle(Duration.ofNanos(4));
>
>        // create throttled observable:
>        ObservableValue<String> binding = source.throttle(throttler);
>
>        assertChanges(
>          binding,
>          "--a-b--c---d-----e-------f-g-----------f-g-----",
>          "--a---b---c---d---e------f---g---------f---g---"
>        );
>
>        assertInvalidations(
>          binding,
>          "--a-b--c---d-----e-------f-g-----------f-g-----",
>          "--i---i---i---i---i------i---i---------i---i---"
>        );
>      }
>
> Thanks for reading, I look forward to your feedback!
>
> --John
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20230328/c4150aec/attachment.htm>


More information about the openjfx-dev mailing list