A practical use of a channel in Amsterdam

The other day I was looking at writing a simple file watcher in Kotlin/JVM. Quick search around pointed me to WatchService — this API has been available since Java 7 in the java.nio.file package. It allows you to get file change notifications and you can assume it uses native calls under the hood for the best performance (… well kind of).

The Standard Way

I started with looking at some Java samples and porting it to Kotlin code — wanted to watch changes in the current directory (that is the directory where the app/unit tests are run from):

Using WatchService API as it is

It worked— very quickly though the existing Java API started to look a bit counter intuitive to me:

WatchService instance is obtained using: FileSystems.getDefault().newWatchService()

instance is obtained using: FileSystems.getDefault().newWatchService() WatchService isn’t the one handling registration — instead you need a Path instance of a directory you want to observe and then you call Path.register passing it the aforementioned WatchService

isn’t the one handling registration — instead you need a instance of a directory you want to observe and then you call Path.register passing it the aforementioned In order to unregister you need to store result of register which is called WatchKey and call close on it — apparently having Path.unregister for some symmetry wasn’t an option.

and call close on it — apparently having Path.unregister for some symmetry wasn’t an option. The WatchKey you got in previous call is not really good for anything else than unregistering. You’ll need another WatchKey to get to file system events by calling WatchService.take — this method waits till new events appear.

you got in previous call is not really good for anything else than unregistering. You’ll need another to get to file system events by calling WatchService.take — this method waits till new events appear. Once events appear, you’ll need to call WatchKey.pollEvents to iterate over list of WatchEvent objects which describe in detail what happened.

objects which describe in detail what happened. The ceremony of waiting for new events requires you to implement some kind of a loop, also don’t forget about calling WatchKey.reset.

By default subdirectories are not monitored. More directories means more Paths which means more WatchKeys. There’s a helper method for that:

Remember to close keys afterwards

WatchEvent isn’t very good at providing you the absolute path to the change that has occurred. It gives you relative value to the Path it was registered with which if you start getting into subdirectories isn’t really that helpful.

isn’t very good at providing you the absolute path to the change that has occurred. It gives you relative value to the Path it was registered with which if you start getting into subdirectories isn’t really that helpful. On top of that you need extra logic to handle subfolders being created or deleted.

The Kotlin Way

As you can see, it takes quite some code to watch for file changes. Kotlin is known for removing the boilerplate. So what could we do? Well, how about we turn the file into a channel? This is what I envisioned better API could look like:

At the time of writing this article ReceiveChannel.consumeEach is marked as obsolete (issue 254) but appears this won’t be happening anymore (issue 1080)

Cute. Now how do we make this work?

First we use an extension function to add asWatchChannel method to the File class. The WatchService always expects a directory. We on the other hand can make it better— figure out if you want to watch folder with its subtree or just an individual file based on the path you pass.

method to the File class. The always expects a directory. We on the other hand can make it better— figure out if you want to watch folder with its subtree or just an individual file based on the path you pass. Our extension returns a channel. We’ll create a new implementation for it (let’s name it KWatchChannel) and move all the boilerplate code inside that class.

We need to free up any resources once we’re done with watching for file changes. We can use Channel.close() as a hook for that.

Now, how do we implement that KWatchChannel? On a very basic level we need to know what our channel will be sending — let’s name that thing KWatchEvent:

It’s a data class in which we’re going to mirror existing WatchEvent API …with few alterations — rather than using Path, we’ll use more popular File. Why do we even have both? 🤷 Also, we’ll be replacing StandardWatchEventKinds with a̵ ̵s̵e̵a̵l̵e̵d̵ ̵c̵l̵a̵s̵s̵ an enum and appropriately named enumerations.

OK, so now that we know what we will be sending from our channel let’s get back to its implementation:

KWatchChannel constructor parameters

A few things are happening here:

We definitely need to have a File as a constructor parameter, we’ll need this information later to setup WatchService and all the WatchKeys

We’ll be using send to emit events with our channel — send is a suspending function thus it needs a CoroutineScope. By default we’ll use GlobalScope for it but if you’re into structured concurrency (and you should be) you will likely want to pass your own scope.

to emit events with our channel — is a suspending function thus it needs a CoroutineScope. By default we’ll use GlobalScope for it but if you’re into structured concurrency (and you should be) you will likely want to pass your own scope. Our channel can work in one of the three modes: watching a single file, watching a single directory or watching directory tree recursively. We pass this information as mode and we’ll be using another enum for it again:

KWatchChannel’s modes

Last but not least we’re going to use a thing called implementation by delegation. Simply put we create an instance of Channel<KWatchEvent> , pass it in the constructor, and have our class wrap every method of that instance without actually writing all that wrapping code — we’ll only need to do wrapping for one method (close) to add some extra functionality on top of it. This manoeuvre greatly shortcuts development time.

Moving next to the init block we’ll be setting up the sending loop:

Removed some implementation details for readability

As mentioned before, the WatchService boilerplate code was moved to KWatchChannel. Since we’re dealing with IO calls here, we’ll use Dispatchers.IO to launch our coroutine. We use launch because we don’t want to have a blocking while loop in the constructor code and also because Channel.send is a suspending function that needs that CoroutineScope.

Finally, once we no longer need our channel, we should be nice and free up any remaining resources.

Nothing fancy here, registeredKeys are all the keys we have collected when registering to subfolder file notifications — we go over each one to cancel all the registrations and clear the registeredKeys array afterwards. In the last line we simply do what a normal wrapping method would do — call its own implementation.

Summary

Kotlin has amazing features and it pays off to know how to use them. I hope in this example I managed to explain how you can use:

extension functions to enhance existing API

channels to manage lifecycle of resources and remove some boilerplate code

implementation by delegation to avoid writing wrapper boilerplate

All the related code is available as a GitHub project👇

Import it via jitpack or just copy paste this one file. If you spot any mistakes leave a note in the comments, raise an issue or make a pull request.