The Word Hunter app

The starter code provided to you for the Word Hunter app is a simple dictionary application:

  • The user is allowed to type words in the text field. The words must be separated by spaces.
  • When the user presses the Search for definitions button, the app will perform a dictionary look up of all those words.
  • An API (https://dictionaryapi.dev) is used to retrieve the definitions. Therefore, there needs to be an internet connection for the app to work.
  • When the results are available, every word will have its own entry in the Accordion.
  • There is a progress bar that should be gradually increasing as results come in.
  • There is a bouncing ball. It doesn’t play any functional purpose, but we use it to inform us whether we have a responsive GUI or not.



Blocking/freezing GUI

Here’s a run through of the app given to you:

Edit

Problems with this app:

This app has many problems that makes it terrible. The UI is ugly and boring. Yes, completely agree. But that’s not our focus for today.

Today, we want to focus on another problem. When the user hits the Search for definitions button, we notice that the entire GUI freezes:

  • The ball stops bouncing around.
  • The Search for definitions button remains “pressed down”, as if it’s stuck.
  • The user isn’t able to edit the content of the text field in preparation for another search.
  • The progress bar doesn’t reveal any progress! It’s useless in fact:
    • In the first search (for “Riddle Contract Second”), it jumps from 0% to 100%!
    • In the second search (for “First Happy Software Development Creation”), it stayed at 100% the whole time (i.e., it didn’t reset to 0% for the new search).
  • As results gradually come in, this is not reflected in the GUI. The Accordion does not gradually populate with results. Instead, everything suddenly appears when it’s 100% done. It would be nice if the user can see definitions for words as they roll in.
  • Even if we had a “cancel” button, it wouldn’t actually allow the user to press it while the search is taking place. Meaning, the user will remain waiting without any idea of how much longer it’s going to take (or even whether it’s doing anything at all!).


Using concurrency for GUI responsiveness

We never want to create a GUI that freezes. It must always remain responsive. This allows the user to do other tasks in the meantime, and also to see progress of the current task. Even if there is nothing else that the user could do with the app (while the task is executing), we still want the app to make it clear it isn’t “hanging”.

Here’s the result we would like to achieve:

Edit

What makes it responsive?

This version correctly uses GUI concurrency to ensure that the app always remains responsive. Key observations include:

  • The ball is always bouncing around—even when a search is taking place.
  • The Search for definitions button becomes disabled. This isn’t the same as before, when it was “pressed down” as if it’s stuck. Here, we purposely disabled it so the user is aware that they cannot do another search until the current search is over.
  • The user is allowed to edit the content of the text field in preparation for the next search.
  • The progress bar conveys how far through the search we are. When a new search is started, it resets to 0% like we would expect.
  • The Accordion gradually populates with results as they come in. The user doesn’t have to wait until all the results come back.
  • While this app currently doesn’t have a “cancel” button, we can add this if we wanted. Since the app is actually responsive near, it means it has the chance to response to the user clicking on a cancel button.



You might find the documentation regarding JavaFX’s Task class useful:

https://openjfx.io/javadoc/21/javafx.graphics/javafx/concurrent/Task.html



Edit Loading the given FXML in Scene Builder

You might be interested in being able to edit the FXML GUI within Scene Builder.

But when you try to open the wordsearch.fxml file in Scene Builder, you will likely encounter the following error as your Scene Builder doesn’t know what the BouncingBallPane is:

Edit

Follow these steps to fix it:

  • Open Scene Builder (but not on the wordsearch.fxml file).
  • Click on the cogwheel next to the Library search, then select JAR/FXML Manager:
    Edit

  • Click on Add root folder with *.class files:
    Edit

  • Navigate to your project’s target/classes folder, then click Open:
    Edit

  • It should have detected the BouncingBallPane component, then click Import Component:
    Edit

  • You’ll see it’s now installed. Click Close:
    Edit

  • Now open up the wordsearch.fxml file. You’ll notice BouncingBallPane appears in the Custom component section, and the view also appears in the workspace area:
    Edit

Loading your own components in Scene Builder

If you create your own components, you can follow these same steps to have them appear in Scene Builder.

Great tip shared by Connor Hare: If you encounter an error, it might be because your component is throwing an exception when Scene Builder tries to instantiate it. You therefore need to make sure that your component’s constructor doesn’t throw any exceptions. Otherwise, it will fail to load the component, and just not recognise it as a valid component. This means that you need to make sure that all values passed in through the constructor do not result in an exception being thrown when they are null, because Scene Builder will construct the component with all null values.



Edit Get your hands dirty!

  1. Fork the JavaFX GUI Concurrency repository to your GitHub account, then clone your fork.
  2. Import the project into VS Code (see the FAQ for help).
  3. You might be getting some compilation errors, since some dependencies are missing. We can fix this by searching for the missing dependencies:
    Edit

  4. Go to https://mvnrepository.com and search for the missing dependencies org.json and okhttp3. Click one of the latest stable version, often guided by the one with highest downloads. Add the dependencies to the pom.xml file:
    Edit

  5. Run the project using the Maven wrapper. The app should start up.
  6. Perform some search like in the examples above. Notice how the GUI freezes.
  7. If you have a look at the terminal, you will notice results are indeed coming in, even if the GUI doesn’t show this:
    Edit

  8. The app froze for over six seconds! What an absolute disgrace!
  9. The only file that you need to modify is WordSearchController.java. However, you are still welcome to browse the other files to understand how everything works.
  10. The reason why GUIs freeze is because there is something time-consuming that is preoccupying the JavaFX Application Thread. Any idea which code is causing this? You guessed it:
    Edit

  11. This line of code is calling the Dictionary API, which (expectedly) takes time to get the results. We need to move this code to a background thread, so that the JavaFX Application Thread is not the one responsible to execute it. We are also allowed to give this background thread other things to do, but there are limitations on what we can delegate to a thread that is not the JavaFX Application Thread. Review the GUI Concurrency topic if needed.
  12. Create an anonymous class of type javafx.concurrent.Task<Void> surrounding all the code in the searchWords() method after the long startTime = System.currentTimeMillis(); code. It will all go inside the Task’s call() method.
  13. If you run the application again now, you’ll notice it doesn’t freeze. But you might also notice that it’s also not doing the search! Although we have wrapped the code inside a Task, we didn’t give this task to a background thread to work on it! You therefore need to create a new Thread instance (giving it the task in the constructor), and tell that thread to start().
  14. Run the code again. You’ll notice in the terminal that the search is now happening. But we have another problem:
    Edit

  15. The JavaFX runtime has detected that some thread (“Thread-2” in this case) other than the JavaFX Application Thread is trying to access a GUI component (if you scroll down further in the exception stack trace, you’ll see it’s in regards to the the Accordion:
    Edit

  16. Scroll down a little further to try and find more information about which part of your code this corresponds to. The line number might differ in your code, but in this example the most-recent information relevant to our code appears to be coming from line 65:
    Edit

  17. If we go to line 65 (or whatever it is in your project), we see this:
    Edit

  18. It appears that adding the pane (a GUI component of type TitledPane) to the Accordion (also a GUI component), has sparked all this problem. But, why don’t we see the problem on line 63, where the background thread is also interacting with the same TitledPane pane component? The difference is that the Accordion is “live” in the app’s Scene, and that’s the reason behind JavaFX picking on this line.
  19. You therefore need to delegate processing of the live GUI component back to the JavaFX Application Thread. We do this using Platform.runLater(). You will need to wrap the relevant code in a Runnable. Either use an anonymous class, or even better is to use a lambda!
  20. Run the code. All should be working well now.
  21. What happens when a word isn’t found? You will also need to use Platform.runLater() when a word isn’t found (see the ... catch (WordNotFoundException e) ... section.
  22. You might notice however, that we can click the search button twice (or more) while a search is still going on! It’s probably not something we want to support, as then the accordion fills up with duplicates, etc. Update your code such that the button gets disabled when a search starts, and re-enabled when the search ends. You can use the setDisable() method on the button.
  23. There are still some problems. You shouldn’t be updating the progress bar (a GUI component) from the background thread. It’s not just about it might cause more exceptions being thrown, it’s actually bad practice to let the task update GUI components. What if there are other GUI components that are interested in the task’s progress?
  24. Instead, we will have the task update its progress using the updateProgress() method. The task should update its progress at the very beginning to zero, and then gradually as each word in the search completes.
  25. We then need to bind() the progress bar’s progressProperty to the task’s progressProperty. This way, the progress bar will automatically update as the task updates. The progress bar is “listening” to the task, unlike before when the task was directly updating the progress bar (known as coupling, which is bad).
  26. After the search completes, we will need to unbind() the progress bar’s progressProperty so that later we can bind() it to another search task.
  27. We can still improve our code a bit more to make it cleaner and more elegant:
    • (a) Refactor the code inside the for-loop by creating a method called retrieveDefinitionAndShowResult(). Doesn’t this look much nicer?
      Edit
    • (b) There are two cases when we need to “reset” our GUI after a search completes. In either case, we want to unbind() the progress bar and also re-enable the search button. Write a single method to do this (e.g., resetSearch()), and then reuse it in both of the following situations:
      • When the task succeeds: setOnSucceeded(), and
      • When the task fails: setOnFailed().
  28. Extra: If you have completed this and want to practice more, add a cancel button:
    • Hint: You will need a reference to the current search task that is accessible (i.e., in scope) to the cancel button’s handler. So you will want to make the task instance as a field member.
    • Add the cancel button using Scene Builder. Tie it to a handler in the controller that will call cancel() on the task.
    • Update the logic inside the task, to check whether a cancel request has come through (check using isCancelled()). If yes, it needs to end the call() method as soon as possible.
    • Decide what behaviour you want to happen when a task is canceled. Do you want to clear the partial results already in the accordion? What about the task’s progress (i.e., what should the progress bar show)—should it reset to 0% or be considered 100%? Up to you!
    • You’ll want to make sure that the search and cancel buttons have the correct combination of enabling/disabling.
    • You’ll notice the stack trace for the IOException being printed, so you might want to remove that since it’s now expected this happens with the canceling.
    • You might find using setOnCancelled() useful on the task.
  29. Extra: Maybe you would like to add some messages/notifications to the user when certain things happen, like when the search is canceled or completed successfully.