Flutter redefined our approach to cross-platform mobile development.

Rainway is a game streaming app that lets you play the games on your home computer, anywhere. A personal Netflix for your game library, if you will.

We’re actively expanding to new platforms. In March 2019, we knew the next step was building iOS and Android apps, but we lacked the engineers to build both side-by-side.

Have Our Cake and Eat It

We needed a portable solution. Investigating our options, React Native seemed like an obvious choice. The web app was already implemented in React, and we’d get to reuse code in porting it over. But concerns piled up as we looked deeper into it: Our web app uses TypeScript, support for which is lacking in React Native. Additionally, it locks us into a JavaScript engine, which hurts both performance and ease of native code integration.

An app like ours needs a low-friction, low-latency UI that allows for lightning-fast communication with a streaming video decoder. Could we even get that close to the metal without losing the benefits of cross-platform support? Were we doomed to sacrifice development speed in order to achieve the responsiveness we needed?

We needed the best of both worlds, and Flutter gave it to us.

With Rainway and Flutter, it was love at first sight.

Hello, Flutter

Flutter is Google’s new cross-platform UI framework. It lets you write user interface code in Dart, that gets compiled down to beautiful native applications for Android, iOS, web, and soon desktop. On top of that, it flaunts a hot reload development cycle, and ships with a ton of great widgets that speak both Material and iOS-style design languages. This all sounded appealing to our team, and we decided to try it out.

Dart was a new language to us. It’s sufficiently similar to Java that picking it up didn’t prove much of a challenge: we were proficiently writing business logic within a day or two. On the presentation layer, Flutter’s syntax was a little more intimidating, coming from JSX. But that makes sense: without the DOM’s sensible defaults, we’re left to describe the entire layout tree ourselves.

Flutter's syntax (right) is usually more verbose than JSX (left).

But Flutter nicely eased us into all this novelty and widget-nesting. We could tell the Flutter team went to great lengths to make sure of that: part of it is up to the nimble hot-reload-based development cycle, but most of it is the ecosystem.

A Welcoming Framework

Flutter does an excellent job at documenting its own widgets and codebase. Each widget has online documentation laying out all the constructors, parameters, and related classes. This makes figuring out how to use a given widget a breeze.

Even then, it was easy to get overwhelmed by the sheer amount of widgets Flutter provides. But the snappy Widget of the Week videos solve this issue elegantly: every week, the Flutter team releases a narrated, animated, elevator-pitch-style explanation of a widget and the use-case it solves.

The widgets themselves are, frankly, great. They have obvious APIs by design, and coming from web development, it’s refreshing to see them stick to a “do one thing and do it well” philosophy successfully. In particular, the toolkit provided for animation is modular and powerful, and the smooth transitions we get out of it make our app feel great to use. And by presenting sensible defaults, developers are guided into sticking to Material Design’s rules for how interactions and screen transitions should feel.

Cross-Platform UI That Just Works

We wrote and tested the current version of our codebase only with iOS in mind. We had decided we’d switch focus to Android a few more months down the road, after the iOS relase, and first get the native game streaming code running smoothly on one platform.

While writing this article, I figured I’d try setting the build target to Android, just for the heck of it: how much of our code would be broken, and how much would be slightly off before going through and fixing stuff? What would the performance be like?

The whole thing built without a hitch on my phone, and the resulting app felt immersively native and smooth as butter. Only the streaming code remained to be hooked up to Android, and this app was ready to ship.

Giving Back

Much of the packages we use are open-source - even the official Dart Team ones. When we need to tweak something under the hood, we can patch it in, file a pull request, and leave a library more featureful than we found it.

If you ever enable the PascalCase option in json_serializable, or subscribe to a state-change stream in flutter-webrtc, that was us, making Flutter that little bit nicer, for you, the next person giving it a try.

Painless and Fast Native Interoperability

There remains one burning question: in terms of sheer performance, did Flutter bring all the things we hoped?

As far as we can see, the answer is yes. We wrote a native Vulkan video decoder in C++, and Flutter allowed us to integrate it as an iOS-native Objective-C++ plugin, that we talk to from the Dart side via “platform channels”.

The latency when transfering continuous video packets, measured between receiving the packet from WebRTC on the Flutter side and feeding it to the decoder on the native side, is about 0.3 milliseconds. That’s only 2% of the duration of a single frame of 60fps game footage!

Below, you can see Rainway being used with a Bluetooth controller to play Rocket League on an iPad. Of course, the game is actually running on a remote Windows computer. Combining the power of Vulkan and Flutter, stream latency is kept to a minimum.

Now, Let’s Play!

The Rainway for iOS beta launches later this August. Ten thousand lucky users will be selected to be the first to play all their favorite PC games on their iOS devices. If you ask us, Flutter has been a huge success for our product. We hope you give Rainway a shot and let us know if you agree.

If you can’t wait for Rainway to launch on iOS, we welcome you to try our web app in anticipation: play your PC games in the browser, anywhere, for free. Get Rainway today!