Renovating Setup, With Flutter
Senior Software Engineer, Initial Configuration Team
Senior Software Engineer, Initial Configuration Team
Can a setup flow be premium? Can the layout of a wizard convey a brand’s values? Can a mix of haptic, visual, and audio feedback spark joy? Just as our industrial design team works to bring you products that look as good as they sound, the Initial Configuration team aims to bring the premium nature of Sonos to your setup experience.
In our previous blog post, we covered the history of setup at Sonos from a variety of angles: the evolving user experience, the advancing setup technology, and the changing framework design. In this blog, we’ll focus on our use of Google’s open-source UI framework, Flutter. If setup is the “grand entryway” to the Sonos ecosystem, Flutter forms its elegant façade.
As we cover the story of this framework at Sonos, we’ll explain how we came to use it, what it cost us, what it gives us, and where we might take it next. By the end of this post, we’ll evaluate if the investment was worth it and—should you be interested in using Flutter yourself— what you might expect from it in your own application.
Backend (C++) Seeking Frontend (Any)
In the distant year of 2018, designers on the Setup Experience team (now, Initial Configuration) laid out a plan to totally modernize Sonos setup. With “show, don’t tell” as a guiding principle, they built a short demo of their vision featuring concise instructions and dynamic product renders, leaving behind legacy setup’s still wireframes and stale text walls. Watching these mock wizard cards pop up from a brand-new row in Settings and slide around the screen was quite exciting for all of us. Product managers were intrigued by the possibilities. Testers couldn’t wait to try breaking the card-sliding feature. Even developers who would have been content with a Sonos setup command-line interface wanted to give these new designs a shot… but how?
With the Sonos apps’ core business logic in a C++ shared library, any solution would require some careful framing and clever plumbing. Moreover, modern setup would need to look as smooth on Android as on iOS, as at-home on a phone as a tablet. How could we build something so refined with limited resources? It was time to go framework-hunting.
Candidate 1: Platform-Native
Building Android and iOS user interfaces the old-fashioned way has its charms: a traditional design, two sturdy foundations… we wouldn’t even need to spend time remodeling our build system, keeping it well within our starting budget. Better yet, we’d be moving just down the street from the old wizard framework, keeping things familiar for developers on and off our team. With a new coat of paint, a touch of Swift here and a touch of Kotlin there, a platform-native approach would do nicely. But there was a catch.
With two mobile apps to support, the drawbacks of this approach quickly became evident. Much of what the design team presented—from animations to videos to swipeable cards—would require separate plugins and unique tooling per-platform, driving up development costs well beyond any upfront savings. Stung by such ballooning project times in the past, the team turned to a second candidate.
Candidate 2: React Native
However, React has its own baggage—from performance issues and app bloat to platform inconsistencies. While a good option, we didn’t want to stop with React. We needed one more point of comparison.
Candidate 3: Flutter
The curb appeal of our last candidate was obvious from the start. Sporting polished animations, fast performance on both platforms, and the elegant Dart programming language, it was everything we could want from new construction (launching in 2017). The neighborhood was yet another plus—managed by Google and home to major players from iRobot to Tencent—but could we afford the upfront costs of using such a framework? Would retraining developers to use Dart be worth our time?
Another downside loomed large: we weren’t looking to uproot our whole lives over a clean API. Setup has relatives all over the app, from settings to the system tab. Even if we moved wizards over to a new UI framework, the rest of the app must continue to function. Could the setup team really take a jump like this? Flutter had a solution: Add-to-App. With the promise that Flutter could be embedded into an existing app (rather than requiring a new one entirely) we considered our options.
The Big Decision
We’d love to report that the discussion lasted late into the evening, that there were factions for each framework, and that a great battle ensued over the fate of setup UI… but the choice was simple. With user interface mock-ups in front of us we asked, “How could we build this?” With a platform-native approach, we’d need to sacrifice most of the finer details to achieve our timeline. We could ship faster with React, but could we rely on its performance when running over our base app? For all its faults, Flutter was a complete solution—and one we could get excited over. So it was that midway through construction of the C++ side of the wizard framework, a few members of our team began work on Sonos’ first Flutter UI.
Flutter, Meet Sonos
Shortly after integrating Flutter into our app using Add-to-App, we arrived at our first real conundrum: Flutter runs in its own thread, using message channels for external communication. With the “state” of setup sitting in C++ code outside of Flutter’s environment, all our updates and callbacks must take place asynchronously: no data binding and no guarantees about message order. Working around this restriction has required mindful design on the C++ side, but hasn’t limited our overall capabilities.
The most unexpected compromise came with the Page<T> class. Flutter, as one might expect, manages its own page stack: if you transition from page A to page B, Flutter knows that backing up will return you right back to A. Unfortunately, our wizard flows are seldom so straightforward. We can’t guarantee that if page A leads to page B, backing out of B will lead to A, and therefore we needed an entirely new solution—building our own page class and transition system. The customization, while initially tedious, has paid dividends as we’ve tailored every part of our page transitions to our own visual style.
Baseline Flutter: Set to Stun
We were sold on Flutter's visuals, and can happily attest we were not one bit oversold. Implementing our designers’ vision was finally possible—and on more than a few occasions, developers have been able to suggest solutions designers wouldn’t have dreamed possible. Flutter’s API is well-designed, and its widget hierarchy sturdy. Adding accessibility text is a breeze, just as is rounding a corner or adding a tint. Flutter even has "hero” tags to smoothly animate visual elements between screens, even as the screens themselves move.
While performance in debug mode might leave something to be desired, release mode Flutter runs as smoothly as any UI—even when running over the Sonos app’s existing interface! We’ve found that after a bit of Dart training, developers can create components to match spec quickly and efficiently, particularly with Flutter’s hot reload feature. With the Flutter engine attached in debug mode, any developer can rebuild Flutter UIs on the fly, even as setup runs under the hood.
A Plugin For This, A Package For That
Of course, Flutter’s benefits don’t stop where the main framework ends: a variety of plugins have proven their worth in setup. Rive (née Flare) animations and product-render videos add motion to what would have been static line art in the days of old setup. The markdown package has allowed us to easily add bold, underline, and link support while the webview plugin has supported the addition of “help sheet” UIs consistent with the rest of our Flutter-wizard setup experience.
It's worth mentioning that help sheets are our first example of a Flutter UI outside of wizards, pushed onto a separate Flutter “route” and able to operate independently. Two interfaces in one framework, all above an existing platform UI—a hint at the wide array of possibilities with Flutter, even in an Add-to-App scenario.
Here Come The Bots
With a large matrix of product models, mobile devices, and room configurations to support, Sonos setup requires a great deal of manual regression testing. While small steps had been taken toward automating select wizard flows before, the setup modernization project gave our automation team an opportunity to make one giant leap toward full coverage.
Impressed by the cleanliness, reliability, and flexibility of Flutter's automation APIs, the team set to work pairing Flutter's automation test hook with debugging tools in our C++ codebase. The result was a robust framework for automated setup testing—allowing any wizard flow to be confirmed on any platform, so long as the proper test cases are written.
While enabling automation was a key goal of setup modernization from the start, we didn’t anticipate how much our automation team would take a liking to Flutter as a tool for building great UIs. Beyond using Flutter to automate wizard flows, they’ve built a desktop tool (yes, Flutter runs on desktop!) for testers and automation alike. This program—capable of running a battery of tests at the push of a button—makes for yet a third application of Flutter at Sonos, entirely independent of our consumer app.
The Bad & The Ugly
After so much doting on Flutter for its best qualities, it’s worth covering a few of its shortcomings. While we can’t vouch for the ease of React’s integration with existing apps, integrating Flutter into our app’s unique build process with Add-to-App proved a serious challenge. For those looking to add Flutter to an existing iOS, Android, macOS or Windows project, know that your mileage will vary entirely with your project’s existing configuration.
Once integrated, we found that despite Flutter’s buttery-smooth performance in release builds, it can be sluggish in debug mode, apparently the result of its powerful debugging tools such as hot reload. While this is not usually an impediment to development, it does require us to re-confirm animations and videos on release builds. Further, while we periodically require new features from the Flutter framework, the vast majority of our Flutter upgrades are required to keep up with new versions of iOS and Android—and those new Flutter versions periodically break old features. We've found ourselves logging bugs on the framework and its plugins more than a few times, and custom-patching issues ourselves as required until the respective teams can get around to it. So, are the headaches really worth it?
Our Final Verdict
Flutter was at the vanguard of our 2020 setup revolution, the most visible of the Initial Configuration team’s many improvements, and the focus of many more today. While our team still requires Android and iOS expertise for dealing with permissions, networking, and platform-specific functionality, setup UI code is now neatly consolidated in a single Flutter layer. As alluded to before, Flutter isn’t limited to wizards within our app: “help sheets” are yet another fluttering species in the greater Sonos ecosystem. Only time will tell where our app will take Flutter and only time will tell where Flutter will take our app.
While the Flutter framework is by no means perfect, it is not an exaggeration to say that it has unlocked a degree of “premium” unlike anything our team had delivered before. Most important to our designers, the ease with which new UIs can be built means that our team spends less time saying “no” to specs and more time iterating on them. If this sounds worth it, we’d recommend giving Flutter a try—we’re glad we did.
Continue reading in User Experience:
Continue reading in Software: