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.
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.
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!
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:
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.