Kotlin Multiplatform at Philo
Philo’s Engineering department loves boring.
We like boring on-call rotations, boring tech stacks, and boring code reviews. Today we’re excited to shed some light on a project that went against every boring-loving bone in our bodies: accelerating client app development with a common code library built on Kotlin Multiplatform.
At Philo, we pride ourselves on delivering a top-notch streaming experience with a very small engineering team. Many of us have been working on this product since we first launched in 2017, but over the years, the team has grown and our technology and approach has evolved.
We’re going to detail some of our frontend development technologies — specifically how and why we adopted Kotlin Multiplatform (KMP) for sharing business logic across five out of our six frontend codebases. Kotlin Multiplatform is a technology that allows us to write our most complex frontend code once and ship it to multiple platforms. This workflow allows us to ship high quality code faster and more efficiently, and when we do have a bug, lets us fix it just once across all our platforms.
With Google’s recent endorsement of Kotlin Multiplatform for sharing business logic across mobile, web, server, and desktop, the timing couldn’t be better for us to reflect on our journey and the benefits we’ve reaped. We feel pleased and encouraged by their announcement that Google Docs is adopting this technology, and we’re excited to see what the future holds for KMP at scale.
Over the next few weeks we’ll release a few posts with details about our journey with KMP. This first post will give a little bit of background and go into detail about how we made the technology decision. In subsequent sections we’ll detail our architecture, some of the challenges we encountered, and provide an outlook on our overall plans for KMP at Philo.
What is frontend business logic and why do we need it?
Imagine you are building a web page that displays some information about a live airing of a TV episode. Maybe it is SpongeBob: S13 | E24 Swimming Fools; The Goobfather airing on 10/21/2015 at 12:00 PM on Nicktoons. On your web page, you would like to display the following information:
- The title of the episode formatted as SpongeBob: S13 | E24 Swimming Fools; The Goobfather
- A background image
- Some information about when the episode aired (or is airing):
- If the airing has not yet aired, dim the image and:
- If it airs today, display today at 12pm
- Otherwise, display 10/21 at 1pm
- If the airing is currently live, display how far into the episode the live airing we are as a percentage (eg if is 12:15pm, it should show 50%).
- If the airing already aired, dim the image and:
- If it aired today, dim the background image, display today at 12pm
- If it aired yesterday display yesterday at 12pm
- Otherwise display 10/21 at 1pm
- If the airing has not yet aired, dim the image and:
Wow, that’s a lot of rules! Maybe it sounds silly, but in our logic we really do have some very complex date formatting rules!
Now, something needs to apply all of those rules!
Naively (and, as a Web 1.0 child, relatable), one might assume that our web server could easily apply the rules each time the web page loads! We’d just return a nice static web page with fully rendered UI. Piece of cake.
The only problem is that the world is spinning rapidly around the sun, and time moves forward. What happens if the user loads the page at 11:59 AM, and the minute rolls over 12:00 PM. Surely we don’t want our users to be confused about the currently live airing episode! We have two options:
- Refresh the page every minute.
- Fetch some raw data from the server, and apply our business logic client-side.
At our scale, refreshing every page with a date on it every minute would be quite costly. So, sadly, the ‘90s are over, and we need to write some Javascript. The Javascript might fetch some data that looks like this:
{ "displayTitle": "**S13 | E24 Swimming Fools; The Goobfather", "backgroundImageUrl": "https://images.philo.com/spongebob.jpg", "airingTime": "2015-10-21T12:00:00+00:00" }
And now, it sets a timer, checks the current time, and re-renders the page based on our date rules every few minutes. The nice thing is that the data fetched from the server is cache-able here, and we don’t need to refresh data we’re displaying.
We’ll spare you the pretend code, but there is a fair bit of it, and what is it doing? It is applying business logic on the client side!
At Philo a lot of the data the user sees on the screen is determined by the backend. We have a whole “backend-driven-ui” framework, which is a topic for another post. (Spoiler: it is named Gosling, after the movie Drive). We apply as much business logic as possible on the backend, but there are three broad areas where business logic must be applied on the frontend:
- Time-based logic
- Cache invalidation (eg, we need to bust a cache because of some event either passed to us from the server via a websocket, or something local to the client)
- Munging together data that is only available client side with data that comes from our server (think client-side SDK like Casting or in-app-purchases).
(1) and (3) are a doozy, because the video player, where our users spend the majority of their, has a lot of time-based logic that combines data from client-side code (like Shaka and ExoPlayer) with server side data. It is a perfect storm of business logic that cannot be pushed off to the server, and we need it implemented six times — once for each platform.
Why We Chose Kotlin Multiplatform
Supporting a wide array of client platforms — Roku, FireTV, Android, iOS, tvOS, Web, Chromecast, and various web-TV platforms — has always been a core challenge for us.
Early on, when Philo was very small, we used React Native to share UI and business logic across iOS, web, and FireTV. This worked! But painfully, and over time it turned into a tangled web of if statements at the UI level: if FireTV: render this UI, if iOS: render this UI: if web render this UI
. We also hit a lot of challenges with D-pad navigation on the connected TV platforms, and ultimately we ripped out RN, and rewrote everything natively.
As the company grew, we built teams around each platform and scaled them out. This worked well when we were small since there was institutional knowledge about how the business logic of the apps worked, but as we grew it began to fall apart. Shipping small features took a long time, and very few people on each team fully understood the business logic.
When we realized we were having trouble scaling with our existing architecture, we decided to embark on a project to adopt code sharing. We had two options: (1) go back to a framework that “does it all” allowing us to share UI and business logic (eg Flutter or React Native), or (2) adopt a framework that allows us to share business logic, but keep the UI native. We opted for the second approach for a few reasons:
- We felt that native app developers would always be able to create more beautiful, immersive UI than we could via a Framework like RN — utilizing native APIs and components produces, in our opinion, a more immersive experience.
- There are often situations when it becomes necessary to have different UI (think animations, hover/focus) based on the modality, screen size, and platform. We didn’t want to end up with another set of ugly and bug-prone
if
statements. - If we replaced the business logic layer with shared code, but left the UI layer alone, we could (in theory) avoid rewriting the entire app. We could incrementally replace business logic layers throughout the app.
Having shared business logic with distinct UI felt like just the ticket!
We set out to share our business logic, with the target of shipping a shared library to our Android, Apple, Chromecast, Web, and Web-TV codebases.
You’ll note that Roku is conspicuously missing from that list. This is a compromise we needed to make. Roku uses an obscure language called BrightScript, which looks a lot like BASIC, and does not interoperate with any other languages. Somebody wrote a WASM→BrightScript library, but it is not fully featured, and not very performant. We would love to see Roku roll out better support for sidecar code, but for now, we have to live with Roku floating out on its own 😟. Still, writing everything twice is better than writing everything 6+ times.
We try to take on any technology decision as skeptics, who, in general, prefer boring and battle tested over shiny and nascent. So we surveyed the landscape with the following constraints:
- The technology needed to be able to ship to JS, Apple and Android
- We preferred to use a language frontend that developers were already familiar with
- We are performance sensitive. Many of our apps run on very low powered devices
With those constraints in mind, we considered:
- C++ → WASM + JNI + Apple native. Unfortunately, not many of our frontend devs write C++, even though it is has improved a lot of the years. Also, some of our web-tv platforms run very old Chromium that does not have WASM support
- Golang → WASM + go-mobile: We felt go-mobile was a bit too experimental, and the WASM piece faced the same challenge as C++.
- Haxe: We liked Haxe, but felt the concurrency primitives were not as nice as Kotlin, and we would’ve needed to build all of the networking pieces from the ground up.
- Kotlin Multiplatform: This checked a lot of boxes for us: it supports JS, Apple (via Objective-C), and Android. Our devs already use it, and the library support was really good. Plus it has great concurrency primitives via coroutines! We use apollo-kotlin in our Android project already and it has multiplatform support! In a dream world, we’d be able to copy/paste code out of our Android project and just use it! (Spoiler alert: this is fantasy.)
We felt that despite our aversion to shiny new things, in this case the sparkles had enough upside to make it worth a try. So, we embarked on a proof of concept.
We started by building out our “player picker” (the little icon that allows you to cast to your TV on some devices) as a KMP component on Apple and Web. This felt like a nice, self-contained project that also offered sufficient complexity — combining platform-specific code with complex-networking flows. We targeted two non-Android platforms, since we understood how Kotlin worked on JVM-based targets.
To measure success we looked at:
- How easy was it to rip out client code and replace it with the library?
- How easy is it to add new features — particularly features that are not consistent across platforms?
- Are there bits and pieces of business logic that don’t fit cleanly into the framework?
- How does it affect performance?
The POC answered all four of those questions. The team felt that implementing the library on the client was relatively straightforward, and that it improved the development lifecycle overall. With that said, performance was not a slam dunk, particularly on our Javascript based platforms. We spent quite a bit of time focused on that and a few other pain points before we were ready to productionize this technology.
Stay tuned for the next post about how we got past those challenges!
If you found this interesting either because you want to come work with us (!), or because you’re thinking about adopting KMP at your org, please feel free to drop us an email seth at philo dot com
or mark at philo dot com
. We’re always happy to chat more about our architecture!