Предотвратите зависание вашего приложения и разочарование пользователей

Программное обеспечение, работающее в одном потоке, в наши дни встречается довольно редко. Мы часто создаем приложение, которое использует API в Интернете и имеет графический интерфейс пользователя (GUI). Если мы обрабатываем взаимодействие с пользователем, делаем вызовы API и обновляем графический интерфейс в одном и том же потоке, наше приложение часто будет казаться «замороженным». Это довольно больно. К счастью, эту проблему можно решить с помощью фоновых потоков.

Давайте посмотрим на следующий пример.

Ваш дядя Гарольд хочет проверить свою электронную почту, но находит Outlook очень запутанным. Он спрашивает вас, свою любимую племянницу или племянника, изучающего компьютерные науки, можно ли получить доступ к его электронной почте одним нажатием кнопки. Поскольку Гарольд — хороший парень, вы решаете что-нибудь для него состряпать.

Требования кристально чистые. Ваш клиент хочет увидеть свою электронную почту, нажав на кнопку. Это означает, что нам нужно создать приложение с кнопкой и списком.

Вот макет для нашего почтового клиента:

А теперь давайте напишем код.

Для простоты напишем приложение на Java с использованием библиотеки JavaFX.

Данные

Электронное письмо должно иметь следующие поля:

  • Отправитель
  • Получатель
  • Предмет
  • Содержание

Поскольку мы делаем наш почтовый клиент только для Гарольда, мы опустим поле receiver.

Вот POJO (обычный старый объект Java), который мы будем использовать для представления электронной почты:

public class Email {
    private final String sender;
    private final String subject;
    private final String message;

    public Email(String sender, String subject, String message) {
        this.sender = sender;
        this.subject = subject;
        this.message = message;
    }

    public String getSender() {
        return sender;
    }

    public String getSubject() {
        return subject;
    }

    public String getMessage() {
        return message;
    }
}

Почтовый клиент

Мы получим электронную почту Гарольда через почтовый клиент. Для Гарольда мы сделаем «фальшивый» клиент, который ждет несколько секунд, прежде чем ответить заранее заданным списком адресов электронной почты.

public class EmailClient {
    private final long delayBeforeResponseInMs;

    public EmailClient(long delayBeforeResponseInMs) {
        this.delayBeforeResponseInMs = delayBeforeResponseInMs;
    }

    public List<Email> fetchEmail() {
        // Harold doesn't know it, but we won't really fetch email...
        // We'll just pretend.
        try {
            Thread.sleep(delayBeforeResponseInMs);
        } catch (InterruptedException error) {
            System.err.println("Hmm... that didn't work");
        }

        return List.of(
                new Email("Red Green", "The Handyman's Secret Weapon", "Duct tape can fix just about anything! If women don't find you handsome, they should at least find you handy."),
                new Email("Winston Rothschild III", "Rothschild's Sewage and Septic Sucking Services", "We're number one in the number two business. We'll take that smell off your hands. We come in a truck and leave in a daze."),
                new Email("Nigerian Prince", "Let Me Share a Fortune with You", "Hey! I want to share my money. Want some? Give me your bank account number, SIN, etc. and I'll send you a few million dollars!"));
    }
}

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

Я почти уверен, что Гарольд будет в восторге, услышав несколько мудрых слов от Рэда и прочитав лозунги компании Уинстона. Однако мы должны предупредить его о мошенничестве с предоплатой…

Графический пользовательский интерфейс

Мы будем использовать таблицу для отображения электронной почты. В нем будет три столбца, по одному для каждого поля. Вот метод, используемый для создания таблицы:

private TableView<Email> createTable() {
    TableColumn<Email, String> senderColumn = new TableColumn<>("Sender");
    senderColumn.setCellValueFactory(dataFeatures -> new SimpleObjectProperty<>(dataFeatures.getValue().getSender()));

    TableColumn<Email, String> subjectColumn = new TableColumn<>("Subject");
    subjectColumn.setCellValueFactory(dataFeatures -> new SimpleObjectProperty<>(dataFeatures.getValue().getSubject()));

    TableColumn<Email, String> messageColumn = new TableColumn<>("Message");
    messageColumn.setCellValueFactory(dataFeatures -> new SimpleObjectProperty<>(dataFeatures.getValue().getMessage()));

    TableView<Email> table = new TableView<>();
    List.of(senderColumn, subjectColumn, messageColumn).forEach(column -> table.getColumns().add(column));

    return table;
}

Содержимое таблицы происходит из наблюдаемого списка. При изменении содержимого списка таблица будет обновляться автоматически.

ObservableList<Email> emailToDisplay = FXCollections.observableArrayList();

TableView<Email> theTable = createTable();
theTable.setItems(emailToDisplay);

Мы будем использовать наш почтовый клиент для получения содержимого, которое будет добавлено в список. Звонок клиенту будет запущен, когда Гарольд нажмет кнопку.

Button theButton = new Button("Download Email");
theButton.setOnAction(event -> emailToDisplay.setAll(client.fetchEmail()));

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

public class EmailClientForHarold extends Application {
    private static final long DELAY_BEFORE_RESPONSE_IN_MS = 5000;

    @Override
    public void start(Stage primaryStage) {
        EmailClient client = new EmailClient(DELAY_BEFORE_RESPONSE_IN_MS);

        ObservableList<Email> emailToDisplay = FXCollections.observableArrayList();

        TableView<Email> theTable = createTable();
        theTable.setItems(emailToDisplay);
        VBox.setVgrow(theTable, Priority.ALWAYS);

        Button theButton = new Button("Download Email");
        theButton.setOnAction(event -> emailToDisplay.setAll(client.fetchEmail()));

        primaryStage.setScene(new Scene(new VBox(theTable, theButton)));
        primaryStage.setTitle("Harold's Email Client");
        primaryStage.show();
    }
}

Давайте попробуем

Следующее окно должно появиться, когда мы запустим приложение.

Неплохо, а? Нажмите на кнопку.

Приложение полностью зависает, пока почтовый клиент загружает почту с сервера.

Как это может быть?

Представьте себе ресторан с одним сотрудником. Этот человек принимает заказы, готовит блюда и подает их. Скорее всего, клиенты будут долго ждать, прежде чем получат свою еду.

Ресторан с одним сотрудником представляет приложение и его основной поток, поток приложения JavaFX.

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

Гарольд, вероятно, расстроится, если приложение будет заморожено слишком долго.

Мы не хотим расстраивать Гарольда.

Как мы можем предотвратить зависание приложения? Используя фоновый поток для получения электронной почты.

Антифриз

Использование CompletableFutures должно быть одним из самых элегантных и простых способов запуска задачи в фоновом потоке в Java.

Вот обновленное действие кнопки, которое получает электронную почту в фоновом потоке и переключается обратно в основной поток для обновления содержимого таблицы.

theButton.setOnAction(event -> CompletableFuture.supplyAsync(client::fetchEmail)
        .thenAcceptAsync(emailToDisplay::setAll, Platform::runLater));

Давайте сломаем это.

Статический метод supplyAsync сообщает приложению, что метод fetchEmail нашего клиента будет предоставлять электронное письмо. Он вызовет метод асинхронно, то есть запустит его в фоновом потоке.

Метод supplyAsync возвращает CompletableFuture. Этот объект представляет значение, которое будет доступно в какой-то момент в будущем. Как только вызов метода завершен, future завершен.

Будущей ценностью после завершения станет электронная почта.

CompletableFuture<List<Email>> futureEmail = CompletableFuture.supplyAsync(client::fetchEmail);

Невозможно точно знать, когда электронная почта будет доступна, но мы хотим отобразить их в таблице, когда они будут доступны.

Метод thenAcceptAsync CompletableFuture позволяет нам делать именно то, что мы хотим. Он говорит приложению взять сообщение электронной почты и поместить его в список сообщений электронной почты для отображения. Метод принимает результат, полученный от предыдущего вызова метода в supplyAsync.

CompletableFuture.supplyAsync(client::fetchEmail)
        .thenAcceptAsync(emailToDisplay::setAll, Platform::runLater);

Таблица перерисовывается после изменения содержимого списка emailToDisplay.

Каждая операция, влияющая на графический интерфейс, должна выполняться в потоке приложения JavaFX. Это объясняет второй параметр, который мы передали методу thenAcceptAsync, Platform::runLater. Он говорит приложению вызывать метод в основном потоке приложения.

Давайте снова запустим приложение. Мы должны получить гораздо лучший результат.

Все работает гладко.

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

Это ресторан с двумя работниками. Официант принимает заказы и подает блюда, как только они будут готовы, а повар готовит еду.

И вот оно!

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

Исходный код почтового клиента можно найти в следующем репозитории GitHub: https://github.com/leblancjs/email-client-for-harold.