Beyond the basics.
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?)
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. As a diagram:
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—
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
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.
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 code on GitHub. Next time we'll look at modeling UI elements as Finite State Machines. Stay tuned.