How Flutter helped us make Stadia Controller setup better for users

In this new series of blog posts, we'll be detailing some of the more technically intricate aspects of working on a platform like Stadia and how we've worked to achieve simplicity in a world of complexities for our end users. In this first set of posts we're tackling some challenges specific to Android and iOS, with more to come in the future.

Part three in our series on building Stadia Controller setup in our mobile app with Flutter, Google's cross-platform UI toolkit.

This is the final part in our series. In the first and second posts, we discussed why hardware setup is an engineering and UX challenge, and how our architecture decisions let us meet that challenge. In this post, we’ll describe how our use of Flutter provided big productivity wins, and some of the ways we improved the reliability and approachability of the setup flow.

Flutter let us execute faster and more confidently

Flutter itself played a massive role in building the controller setup flow. Even without using any advanced parts of the SDK, using Flutter let us be confident in the setup flow’s correctness and allowed us to implement it significantly faster. We emphasized code-sharing between iOS and Android as much as possible, made frequent use of hot reload to speed up our development cycle, and leaned on widget and screenshot testing.

Screen capture of the final Stadia Controller setup steps in the mobile app
Writing UI using Flutter’s widget system is quick, and we wrote custom widgets to abstract away a portion of the UI boilerplate and make implementation even faster. Based on our team’s experience, writing in Flutter is as fast or faster than writing the same thing natively, which means we’re seeing at least a 2x productivity increase from using Flutter. If we factor hot reload into the equation, the productivity improvement is greater still, due to the extremely rapid development cycle it unlocks.

One place where codesharing paid dividends is the custom Bluetooth plugin we wrote to communicate with the Stadia Controller. We intentionally made the native iOS and Android layers as thin as possible, with those portions of the plugin only managing the native Bluetooth stack. The bulk of the logic was written in Dart, and was shared between platforms. This greatly reduced the quantity of code that needed to be written. For example, once BLE scanning was implemented on one platform, implementing it on the second was significantly faster because most of the required logic already existed in Dart.

It’s extremely important to note that writing code in Flutter eliminates the potential for bugs related to cross-platform consistency almost entirely. Bugs that would be a near-constant drag on productivity with two or more native clients, especially in a complicated flow, are practically a non-issue when using Flutter.

Our team leaned heavily on widget and screenshot diff tests to validate that the setup flow’s UI looked and behaved correctly, and stayed that way over time. Screenshot tests for each page in the flow have helped to ensure UI consistency between pages, and across different device sizes and orientations. They’ve also caught accidental UI changes on multiple occasions, saving us a lot of grief. Widget tests ensure that things like button taps and text entry are always handled, and handled correctly. Our team insists on a high level of testing and doing so has helped prevent garden-variety bugs (e.g.: “This button isn’t hooked up to anything,” “This screen doesn’t support landscape,”) from creeping into our codebase.

Writing cross-platform code with Flutter, and thoroughly testing it, meant that we were able to quickly implement the Stadia Controller setup flow, and were able to know with much more certainty that the flow behaves as expected.

We increased the time available to focus on reliability and user success

The design choices discussed previously helped us ensure that the controller setup flow is correct, and that under ideal conditions it works properly. However, as was mentioned in the three pillars discussion, achieving correctness is only one step of many in the development process. Because of early decisions the team made when implementing the app and setup flow, we were able to spend a significant amount of time improving the robustness of the flow, and iterating on it to increase the success rate for users.
We implemented queuing and retry logic in our Bluetooth plugin, to maximize the success rate when communicating with the Stadia Controller, and began tweaking Bluetooth configuration settings to improve the performance across a range of devices. These changes reduced the incidence of Bluetooth errors drastically, and made our connection to the controller much more resilient. One memorable afternoon while we were working on this, we started testing the limits of the connection strength at distance. After we succeeded in setting up a controller, with the app running on a phone at a desk, and the controller 75ft (23m) away, across the crowded office, in a conference room with the door closed, we knew weak signal strength was not likely to be an issue.

We also added special error states and failsafes for common issues. We took the time to include more helpful and explanatory error messages, and to describe how to resolve errors. In cases where the controller gets disconnected during setup, we allowed the controller to immediately try to reconnect to the phone to get users back into the setup process quickly. These types of additions make it more likely that a user will be successful going through the setup flow.
Stadia Controller completing the final setup steps
Furthermore, this additional time allowed us to simply uncover more issues. Since some types of failures can be very infrequent, having a large number of setup attempts during internal testing allowed us to gather more data about where folks were getting stuck, and provided more opportunities for particularly tricky bugs to manifest. Discovering these issues early gave the team a chance to fix them, and made us even more confident in the flow’s robustness.

Lastly, we’ve also iterated on the setup flow multiple times in response to user feedback and UX research. Buttons have been renamed, troubleshooting steps and entire sections have been added, and individual screens have been moved around to improve the chance that someone who takes a controller out of the box will set it up successfully on their first try.


The Stadia Controller setup flow is one of the more complicated parts of the Stadia app, but writing and maintaining it is manageable thanks to our use of Flutter, our decisions to use event-driven programming and state machines, and a focus on testing and reliability. Setting up a Stadia controller takes most people less than a minute, and many may never consider how much is going on behind the scenes. While we’ll never stop working to improve the setup experience, to us that level of user success is a great foundation.

--Nick Sparks, Software Engineer
Become a Stadia developer image

Become a Stadia developer

Apply Now

This site uses cookies to analyze traffic and for ads measurement purposes. Learn more about how we use cookies.