Building reactive apps with JavaFX: part 2

In the first part of this series we looked at how ReactiveX can decouple and simplify UI component logic. Now we'll look at implementing deeper functionality that sits at the core of Animation Viewer.

You could say its main function is opening a file, parsing out its frame images, splatting them onto the canvas, and then watching that file for changes. Of course this workflow starts and ends with UI involvement, but there's definitely more to it than incrementing a number. It turns out you can use Observables to tackle this problem as well. (Surprised?)

File management

What happens when a user wants to open a file? Using techniques similar to those we've already seen, we first convert UI open file actions into a Subject of File objects. Call that newFile. We then need to pipe those files through some logic, called loader, to scrape out the frame info. Loader outputs to the newFrames Subject which our canvas will listen to. Loader is implemented as an Akka Actor because that's a natural fit for computations like this. I wrote some utility functions to easily marry Observables and Actors, but they play together really quite well.

A diagram illustrating UI actions registered as the newFile subject, then piped through loader, with output to the newFrames subject.

File updates, that is when the user edits and saves their image file in a separate editor, happen in a very similar way. We use a separate channel for these—updateFile and updateFrames—so that canvas can handle new files and updated files differently.

A diagram illustrating FileWatcher actions registered as the updateFile subject, then piped through loader, with output to the updateFrames subject.

What's FileWatcher? There's a lovely little library called schwatcher which takes Java's file watching API and wraps it into an Actor- and ReactiveX-friendly package. (Really glad I didn't have to write that myself!) The only thing we have to do is point it at a file when we open a new one. Turns out we already have a Subject for that!

Combining the previous two diagrams: FileWatcher is triggered by input from the newFile subject.

Now we could stop there but that's all too simple, wouldn't you agree? Animation Viewer has another feature where it filters out frames that are named by a certain convention. This way users can exclude frames that aren't really relevant to the animation. That would normally be a concern for the loader logic only, but what if we want to allow users to change the filter? (I never actually coded that feature, but I left the infrastructure in place to support it in case I ever do. You'll see it's pretty low-cost, so why not?)

First we need to inline the current filters into this workflow. Naturally we make a new Subject frameFilter to store that. When the filters are changed, that should trigger a published result to updateFrames, but not newFrames. We can work it into the system like this:

Adding to the previous diagram: the frameFilter subject is combined with the updateFile subject via a combineLatest operator before being piped through loader to produce the updateFrames subject. frameFilter is also included in the newFile-to-newFrames path via caching logic named MostRecent and the map operator.

combineLatest is an operator that combines two Observables by publishing an update whenever either one is updated. It wraps the new value from one and the most recent value from the other into a tuple and sends it along. This is perfect for updateFrames, but newFrames needs a slightly different approach. Changing the filters shouldn't trigger a newFrames message, so instead I wrote a little utility class called MostRecent which keeps track of the value of an Observable and allows you to fetch it on demand. This way we can inline the latest value of frameFilter without responding to its changes as they happen.

This way the loader actor is always handed a file and the most recent filters to apply to that file.

Here's the code listing for this file management workflow. (Or view it on gist.)

package com.ornithoptergames.psav

import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import com.beachape.filemanagement.RxMonitor
import com.ornithoptergames.psav.FrameInfoLoader._
import com.ornithoptergames.psav.Messages._
import com.ornithoptergames.psav.RxMessage.Implicits._
import akka.actor.ActorSystem
import akka.util.Timeout
import com.beachape.filemanagement.Messages.EventAtPath

class FileManager(implicit system: ActorSystem) {
  
  // fileWatcher will monitor a path and emit the file when it's modified.
  val fileWatcher = RxMonitor()
  
  // When we see a new file, change fileWatcher's watching path.
  newFile.subscribe { file =>
    // Registration is "bossy" by default here:
    // don't have to worry about unregistering old paths.
    fileWatcher.registerPath(ENTRY_MODIFY, file.toPath())
  }
  
  // When fileWatcher sees a reload, publish an updated file.
  fileWatcher.observable.forwardTo(updateFile, (event: EventAtPath) => event.path.toFile)
  
  implicit val timeout = Timeout(15 seconds)
  val loader = system.actorSelection(system / FrameInfoLoader.actorName)
  
  // When a file is updated or the frame-name exclude list changes,
  // publish updated frames.
  updateFile.combineLatest(frameFilter).map(Load.tupled)
    .pipeThrough(loader, updateFrames, { case t => t.printStackTrace() })
  
  // When a new file is selected, publish new frames.
  val mostRecentFrameFilter = new MostRecent(frameFilter)
  newFile.map(f => Load(f, mostRecentFrameFilter.get))
    .pipeThrough(loader, newFrames, { case t => t.printStackTrace() })
}

Pretty good for only 25 lines or so. I like this example because it demonstrates the power and flexibility of the Observables approach.

I hope this has given you some inspiration to try these techniques in your own work. If you want more detail, you can of course check out the full code on GitHub.