在本文中,我想通过实际示例向您展示RxJava的好处-桌面JavaFx GUI应用程序。 如果您正在开发Android或同时“计算和呈现内容”的任何其他应用,请继续阅读!
If you never heard of JavaFx before don't feel bad. On the other hand, if you thought JavaFx project is long dead, well... I don't blame you. But believe it or not it's alive and kickin'. Since it was open-sourced and separated from JDK it has become the only reasonable choice for building desktop apps in Java.
为什么我在地球上写有关在2020年用Java创建桌面应用程序的技术?
我最初是在2013年被介绍给JavaFx的,当时它仍是JDK的一部分,并且是以前的UI库Swing的适当替代品,但是...让我不受欢迎。 但是JavaFx就像是轻而易举的事,引入了2.0版的概念XML文件文件,可让您以类似于HTML和CSS的方式定义组件的外观和样式。
为什么在地球上仍然有人想在2020年构建桌面应用程序?
有几个原因。 首先,某些用户仍然偏爱基于Web的服务。 如果没有Internet连接,则无法访问该站点(如果该站点未启用脱机功能)。 而且,这类应用程序非常适合与文件系统或底层OS进行通信(例如,我们有一个通过ssh连接并执行脚本的实用程序,其结果显示在UI中)。 我认为那里有很多好处,但这不是今天的主题。
假设您正在用JavaFx编写简单的UI。 您定义布局,创建第一个组件,开始向其添加行为,然后您期望得到结果。 从技术上讲,一切正常,但是有时候感觉不对。 执行操作时,UI滞后,您会觉得自己做错了什么。
为了说明这一点,我创建了一个仅包含两个组件的简单应用程序:
执行此任务的代码非常简单(忽略结果现在,这只是一个简单的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");
}
}
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将用作列表显示。
对于不熟悉JavaFx的人,列表显示与特殊的收集包装器一起使用(ObservableList)与组件绑定。 因此,无论何时更改其内容,UI中的内容也会随之更改。 就那么简单。
嘿! 由于是2020年,我们将使用流s,可大大提高所发生事件的可读性:
private void runTasksJavaFx(ObservableList<String> 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
}
private void runTasksJavaFx(ObservableList<String> 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
}
这看起来不错,但您可能可以感觉那是不对的。 尝试时,您将获得:
如您所见,当选择了item时,整个GUI会冻结,直到计算完成(您可以在右侧的控制台输出中看到它)。
这不仅是JavaFx的问题,而且肯定也是Swing的噩梦。 这里的问题是应用程序代码与UI代码在同一线程上运行,这意味着每当有一些长期运行的任务它将阻止UI更新,直到完成。 这就是上面正在发生的事情。
当然,有解决此问题的方法。 其中之一是使用JavaFxPlatform.runLater()标准的方法可运行作为单个参数。 它将更新计划为“将来的某个时间”。 其次是使用JavaFx任务并与执行人服务(和相关)。 为了简单起见,我们将使用第一个:
private void runTasksLaterJavaFx(ObservableList<String> 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());
}));
}
private void runTasksLaterJavaFx(ObservableList<String> 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返回虚空,我们需要以“老式方式”处理结果。 我个人的看法:这很丑。 如果逻辑更复杂,它将变得更加难看。
然后,在Platform.runLater方法的文档:
注意:应用程序应避免将太多未决Runnable泛洪到JavaFX。 否则,应用程序可能无法响应。 鼓励应用程序将多个操作分批处理到更少的runLater调用中。 另外,应在可能的情况下在后台线程上执行长时间运行的操作,从而释放JavaFX Application Thread进行GUI操作。
这不是很令人鼓舞,不是吗? 因此,当它变得复杂时,您需要引入用于批处理更新的逻辑。 您可以使用Task / Executors解决方案,但这甚至是更多的代码,如果开发人员讨厌一件事就是阅读大量代码以了解正在发生的事情。
但是,好的,让我们看看执行此方法时的作用:
此处的改进是我们不会阻止UI,但是您可以看到,更新会定期显示在控制台输出中,但是一旦一切完成,它们就会呈现出来。 这当然可以改善,但是要付出什么代价呢?
当然,有比战斗线程更好的选择。 由于我们希望将代码推向21世纪的第三个十年,因此我们将使用响应式扩展(RX)!
With RX it is as with anything that's currently trending like Cloud computing or Blockchain. The concept is not new, it's just a matter of putting everything together to solve a specific problem. The main marketing pitch for RX is The Observer pattern done right. I won't go into too much details about RX so I suggest you to read a thing or two on reactivex.io website.
The Observer pattern in a nutshell is about creating an object that would emit changes (Observable
) and registering some handler that would execute action whenever needed (Observer
). RX goes further with that since it is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming. There are multiple implementations of RX in various languages, so we're going to use RxJava.
我将继续发布一个代码示例,我将逐行详细解释该示例:
private void runTasksRxJavaFx(ObservableList<String> 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
}
private void runTasksRxJavaFx(ObservableList<String> 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
looks very similar to what we have in first example (the blocking one). But instead of using regular IntStream
we are using one of RxJava's utility classes to generate an observable collection. In this case it will emit
each iterated element, so that observers are notified.// 2
is probably the most difficult concept to understand here. This is because RxJava is not multi-threaded by default. To enable multi-threading, you need to use Schedulers
, to off-load the execution. subscribeOn
method is used to describe how you want to schedule your background processing. There are multiple schedulers you can use based on the type of work. Specifically Schedulers.computation()
will use bounded thread-pool with the size of up to number of available cores.// 3
are the same as in first example, see?// 4
is similar to subscribeOn
, but observeOn
instead declares where you want to schedule your updates. If you look at the flow of the code, at this point, we would like to emit
changes back to the UI thread. In this case, we will use special type of scheduler which is part of (additional) RxJavaFx library and it uses - guess what - the Java FX GUI thread.// 5
is also same as in the first example, but in this step we are actually instructing what should be done after observeOn
was called.最终看起来像用户想要使用的应用程序:
当然还有更多。 RxJavaFx项目具有用于创建RX就绪组件的漂亮API。 被告知学习曲线的真理可能是“温柔的”,尤其是在涉及RxJava本身时。 但是,如果使用得当,则可以对其进行调整以处理大量更改,而无需牺牲用户体验或应用程序的性能。
This was just a fly-by of what you can do with RxJava and I omitted a lot of details (including the setup of JavaFx project), so for those of you that read it until here I have prepared a Git repository with template project that you can use for developing JavaFx applications and a branch with fully working example of what I just described here.