Рубрики
Без рубрики

Использование RxJava для создания реактивного пользовательского интерфейса JavaFX

Улучшение кодирования и пользовательского интерфейса приложений с графическим интерфейсом с помощью RxJava. Помеченный как rxjava, javafx, реактивный, java.

В этой статье я хотел бы показать вам преимущества RxJava на практическом примере – настольном приложении JavaFX с графическим интерфейсом. Если вы разрабатываете Android или любые другие приложения, которые одновременно “вычисляют и отображают контент”, читайте дальше!

Вступление, которое вы можете пропустить, если знаете JavaFX

Если вы никогда раньше не слышали о JavaFX, не расстраивайтесь. С другой стороны, если вы думали, что проект JavaFX давно мертв, что ж… Я тебя не виню. Но хотите верьте, хотите нет, но он жив и здоров” . Поскольку он был с открытым исходным кодом и отделен от JDK, он стал единственным разумным выбором для создания настольных приложений на Java.

С какой стати я пишу о технологии создания настольных приложений на Java в 2020 году?

Я впервые познакомился с JavaFX в 2013 году, когда он все еще был частью JDK и был подходящей заменой Swing, бывшей библиотеке пользовательского интерфейса… скажем так, непопулярный. Но JavaFX был похож на свежий ветерок, и в версии 2.0 появилась концепция XML файлов, которая позволяла вам определять внешний вид и стиль ваших компонентов аналогично HTML и CSS.

С какой стати кому-то вообще захочется создавать настольные приложения в 2020 году?

Есть несколько причин. Во-первых, некоторые пользователи все еще предпочитают его веб-сервисам. Если у вас нет подключения к Интернету, вы не сможете получить доступ к сайту (если он не включен в автономном режиме). Кроме того, такого рода приложения идеально подходят для связи с файловой системой или базовой операционной системой (например, у нас была утилита, которая подключалась через ssh и выполняла скрипт, результаты которого отображались в пользовательском интерфейсе). Я думаю, что там есть много преимуществ, но это не тема для сегодняшнего дня.

Вы хотите разработать приложение?

Допустим, вы пишете простой пользовательский интерфейс на JavaFX. Вы определяете свой макет, создаете первые компоненты, начинаете добавлять к ним поведение и ожидаете результатов. Технически все работает, но иногда это кажется неправильным . Пользовательский интерфейс отстает при выполнении операций, и у вас создается впечатление, что вы делаете что-то неправильно.

Чтобы проиллюстрировать это, я создал простое приложение, состоящее всего из двух компонентов:

  • Выпадающий список (он же. Поле выбора), который будет выполнять некоторые фоновые задачи всякий раз, когда пользователь выбирает другое значение
  • Просмотр списка результатов этих длительных задач. В этом списке будет указано название задачи и время, затраченное на ее выполнение (до 1 секунды) в порядке выполнения. И если время превышает 500 мс, оно будет помечено как медленное .

Код для выполнения этой задачи довольно прост (пока игнорируйте класс Результат , это просто POJO):

private Result runTask(Integer i) {
    long currentTime = System.currentTimeMillis();

    String name = "Task" + i;
    long sleepDuration = (long) (Math.random() * 1000);

    try {
        Thread.sleep(sleepDuration);
        return new Result(name, sleepDuration);
    } catch (Exception e) {
        return new Result("-", 0);
    } finally {
        System.out.println(name + " took " + (System.currentTimeMillis() - currentTime) + " ms");
    }
}

Длительная задача будет довольно простой. Мы определяем NUMBER_OF_TASKS , которые должны быть выполнены, и собираем результаты в ObservableList , который будет использоваться в качестве резервной коллекции для ListView .

Для тех, кто не знаком с JavaFX, ListView работает со специальной оболочкой коллекции ( ObservableList ), которая связана с компонентом. Поэтому всякий раз, когда его содержимое изменяется, оно также изменяется в пользовательском интерфейсе. Вот так просто.

И эй! Поскольку сейчас 2020 год, мы собираемся использовать Stream s, что значительно повысит читабельность происходящего:

private void runTasksJavaFx(ObservableList observableList) {
    IntStream.range(1, NUMBER_OF_TASKS) // Stream API way of iterating
            .mapToObj(this::runTask) // Execute and map the results of our long-running task
            .map(result -> result.time > 500 ? new Result(result.name + " (slow)", result.time) : result) // "Annotate" those that took too long
            .forEach(result -> observableList.add(result.toString())); // And push them to result list so that they are displayed in UI
}

Это выглядит красиво, но вы, вероятно, можете почувствовать , что это неправильно. Когда вы попробуете это, вот что вы получите:

Как вы можете видеть, когда элемент выбран, весь графический интерфейс зависает до завершения вычислений (вы можете видеть это в выводе консоли справа).

Используй нити, Люк

Эта проблема не ограничивается JavaFX, и это, безусловно, было кошмаром и в Swing. Проблема здесь в том, что код приложения выполняется в том же потоке, что и код пользовательского интерфейса, что означает, что всякий раз, когда возникает какая-либо длительная задача он будет блокировать обновления пользовательского интерфейса до тех пор, пока он не будет завершен. И это именно то, что происходит наверху.

Конечно, есть способы это исправить. Одним из них является использование метода Javafx Platform.runLater() , который использует стандартный Запускаемый в качестве одного параметра. Он будет планировать обновления до “некоторого времени в будущем”. Во-вторых, использовать Javafx Задача и вместе с Executorservice (и связанными с ними). Чтобы все было проще, мы будем использовать первый:

private void runTasksLaterJavaFx(ObservableList observableList) {
    IntStream.range(1, NUMBER_OF_TASKS) // Still Java 8, yaaay!
            .forEach(i -> Platform.runLater(() -> { // We're using lambda for Runnable, so we cannot map the result
                Result result = runTask(i); // So we go one Java version down with the code style
                if (result.time > 500) {
                    result = new Result(result.name + " (slow)", result.time);
                }
                observableList.add(result.toString());
            }));
}

Хорошо, здесь есть несколько вещей, и самая важная из них заключается в том, что поскольку Runnable возвращает void , нам нужно обрабатывать результаты “по старинке”. Мое личное мнение: это некрасиво. И это будет еще уродливее, если логика будет более сложной.

И затем есть эта приятная заметка в документации метода Platform.runLater :

ПРИМЕЧАНИЕ: приложениям следует избегать переполнения JavaFX слишком большим количеством ожидающих запуска приложений. В противном случае приложение может перестать отвечать на запросы. Приложениям рекомендуется объединять несколько операций в меньшее количество вызовов runLater. Кроме того, длительные операции должны выполняться в фоновом потоке, где это возможно, освобождая поток приложения JavaFX для операций с графическим интерфейсом.

Это не очень обнадеживает, не так ли? Поэтому, когда это становится сложным, вам нужно ввести логику для пакетной отправки обновлений. Вы могли бы использовать решение задачи/исполнителей, но это еще больше кода, и если есть что-то, что разработчики ненавидят, так это чтение огромного количества кода, чтобы понять, что происходит.

Но хорошо, давайте посмотрим, что он делает, когда мы выполняем этот метод:

Улучшение здесь заключается в том, что мы не блокируем пользовательский интерфейс, но, как вы можете видеть, обновления периодически отображаются на выходе консоли, но они отображаются после того, как все закончено. Это, конечно, можно улучшить, но какой ценой?

Знаешь что, Люк? Давайте еще раз вернемся к этой идее потоков

Есть, конечно, лучшие варианты, чем борьба с потоками. Поскольку мы хотим перенести наш код на третье десятилетие 21 века, мы будем использовать реактивные расширения (RX)!

С RX это так же, как и со всем, что в настоящее время находится в тренде, например, облачные вычисления или блокчейн. Концепция не нова, это просто вопрос объединения всего вместе для решения конкретной проблемы. Основной маркетинговый шаг для RX – это Шаблон наблюдателя сделан правильно . Я не буду вдаваться в подробности о RX, поэтому предлагаю вам прочитать кое-что на reactivex.io веб-сайт.

Шаблон наблюдателя в двух словах заключается в создании объекта, который будет вносить изменения ( Наблюдаемый ) и регистрировать некоторый обработчик, который будет выполнять действие при необходимости ( Наблюдатель ). RX идет дальше в этом, поскольку он представляет собой комбинацию лучших идей из шаблона наблюдателя, шаблона итератора и функционального программирования . Существует множество реализаций RX на разных языках, поэтому мы будем использовать RxJava .

Я продолжу и опубликую пример кода, который я подробно объясню построчно:

private void runTasksRxJavaFx(ObservableList observableList) {
    Observable.range(1, NUMBER_OF_TASKS) // 1
            .subscribeOn(Schedulers.computation()) // 2
            .map(this::runTask) // 3
            .map(result -> result.time > 500 ? new Result(result.name + " (slow)", result.time) : result) // 3
            .observeOn(JavaFxScheduler.platform()) // 4
            .forEach(result -> observableList.add(result.toString())); // 5
}
  • Линия //1 выглядит очень похоже на то, что мы имеем в первом примере (блокирующий). Но вместо того, чтобы использовать обычный IntStream мы используем один из служебных классов RxJava для создания наблюдаемой коллекции. В этом случае он будет выдавать каждый повторяющийся элемент, чтобы наблюдатели были уведомлены.
  • Линия //2 это, пожалуй, самая сложная концепция для понимания здесь. Это связано с тем, что RxJava по умолчанию не является многопоточным . Чтобы включить многопоточность, вам необходимо использовать Планировщики , чтобы разгрузить выполнение. метод subscribe On используется для описания того, как вы хотите запланировать фоновую обработку. Существует несколько планировщиков, которые вы можете использовать в зависимости от типа работы. В частности, Schedulers.computation() будет использовать пул потоков boundedthread размером до количества доступных ядер.
  • Линии, отмеченные //3 такие же, как в первом примере, видите?
  • Линия //4 аналогично подписаться на , но обратите внимание на вместо этого указывает, где вы хотите запланировать свои обновления. Если вы посмотрите на поток кода, на этом этапе мы хотели бы внести изменения обратно в поток пользовательского интерфейса. В этом случае мы будем использовать специальный тип планировщика, который является частью (дополнительного) Библиотека RxJavaFx и она использует – угадайте, что – поток графического интерфейса Java FX.
  • Линия //5 это также то же самое, что и в первом примере, но на этом шаге мы фактически инструктируем, что следует делать после Был вызван observeOn .

И это, наконец, похоже на приложение, которое пользователи хотят использовать:

Конечно, в этом есть нечто большее. Проект Rx JavaFX имеет хороший API для создания компонентов, готовых к RX. По правде говоря, кривая обучения может быть “мягкой”, особенно когда дело доходит до самой RxJava. Но при правильном использовании вы можете настроить его для обработки большого количества изменений без ущерба для пользовательского интерфейса или производительности вашего приложения.

Это был всего лишь краткий обзор того, что вы можете сделать с RxJava, и я опустил много деталей (включая настройку проекта JavaFX), поэтому для тех из вас, кто читал его до сих пор, я подготовил Репозиторий Git с шаблоном проекта, который вы можете использовать для разработки приложений JavaFX и ветка с полностью рабочим примером того, что я только что описал здесь.

Оригинал: “https://dev.to/rapasoft/using-rxjava-for-creating-reactive-javafx-ui-2dj9”