23 February 2022

Flutter on Five Platforms 💙

A deep dive into what I learned building a cross-platform app with Flutter.

Dan Reynolds
Dan Reynolds @thederivative

This past year, I experimented with making a Flutter app for five platforms: iOS, Android, a Chrome extension, desktop web and mobile web.

As a mobile developer who had mostly worked with React Native, I was excited to give Flutter a try and see how it stacked up. The results were really interesting and I wanted to share what I learned about Flutter and how other teams can approach using it to build their own cross-platform apps.

The project is called Pollyn - the referral sharing app, which you can take a look at below if you’re interested in seeing what an example Flutter app looks like on each platform:

A Single Codebase

The Flutter homepage describes it as a framework to build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.

A tagline like that is sure to make a lot of developers excited, while evoking a healthy amount of skepticism from others. A few questions that come to mind include:

  1. How much code actually gets shared?
  2. Does the codebase end up messy and filled with platform checks?

Let’s see how much shared code we actually have!

Results

To keep the code more organized, all of the source files in the project are grouped by platform and form factor.

Folder structure

This approach separates out the different platform implementations and reduces the number of messy checks in files. It also allows us to easily total the number of lines of code for each platform to get a rough estimate of how much of the codebase is shared.

Code share pie chart

Of the ~30k lines of application code, around 41% of it is shared across the entire codebase, including 100% of the data models, constants, utilities and services like authentication. That’s definitely a lot of code reuse, but to take it a step further, we need to clarify the difference between the mobile and desktop form factor.

Mobile form factor:

  • Android and iOS phones
  • Portrait Android and iOS tablets
  • Mobile web
  • Chrome extension

Desktop form factor:

  • Desktop web
  • Landscape Android and iOS tablets

To make our understanding of code sharing more meaningful, we have to take into account that mobile and desktop form factors are designed very differently. For example, here’s what the homepage of the app looks like on mobile versus desktop:

Each form factor can only reuse a certain number of core components like buttons and inputs. The screens and pages themselves will need to be designed differently. By breaking down the the shared code within the mobile form factor, we’re able to see some clearer reusability wins:

Code share pie chart

Of all of the code dedicated for mobile, around 87% of it is shared across all mobile platforms, with around 1% of code written specifically for each of iOS, Android and mobile web. Another 4% of the code is written generically for native, either Android or iOS.

This is a huge reusability win and really lowers the burden to expand the app to new platforms.

Customization by platform is important though, so when support for the Chrome extension was later added, the experience was changed to better reflect how the app would be used on that unique platform.

This involved moving around the bottom navigation, adding in some extension-specific screens and disabling some other features that came for free but didn’t seem useful in that context. Overall, most of the mobile code was still shared for the extension, with around 5% of it written custom.

The idea that you can use the same library to build your entire Android or iOS app and then, with some small changes, bring that experience to web or a Chrome extension is really powerful and something that is currently unique to Flutter.

Where Flutter Takes Flight

Beyond the code reuse benefits, some of the other strengths of the platform that I experienced included:

  1. Great Documentation
    Documentation for Flutter is excellent, featuring a variety of guides, thorough API documentation and plenty of examples including a widget of the week YouTube series that has been running for nearly 3 years.

  2. Massive Widget Library
    Flutter’s user interface is made up of elements called widgets. The official widget catalogue is huge and many times I’ve been surprised to find a widget that exactly matched what I was looking for.

    • Need to transition between two widgets? Try AnimatedCrossFade.

    • Looking to spice up a banner image with a fancy gradient effect? Check out Shader Mask.

    • Not looking forward to prototyping a complicated drag and scroll component that looks like it will be a pain to make? Just use DraggableScrollableSheet.

    There are lots of gems in the widget catalogue to choose from and explore.

  3. Fast UI Development
    Flutter uses a declarative UI style popularized by other frameworks like React.

    One of the benefits of working with a declarative framework is that you only need to describe your UI once. Changes to the application state, like when a user types in an text field, trigger a redraw of the user interface with the updated input.

    declarative-ui

    Many developers find this model easier to work with and less complex than imperative-style frameworks like Android SDK or iOS UIKit.

    Combined with other developer productivity features like hot reloading and integrated debugging in VSCode, building interfaces in Flutter is generally fast and intuitive.

  4. Simpler Dependency Management
    Coming from a JavaScript background, dependency management usually means slow installation times, bloated node_modules folders and community debates over package managers.

    None of these problems have been an issue iny my experience with Pub, the package manager for the Dart programming language that Flutter apps are written in. Dependencies are comparatively straightforward to install, override and upgrade.

    Working with dependencies is made easier through the use of reliable go-to-definition functionality in editors like VSCode and support for breakpointing and editing source implementations while debugging.

    When it comes time to release an application, Flutter generates an optimized release build, which on web platforms includes automatic minification and tree-shaking of all of your dependencies.

Wait, you said Dart?

When I first got started with Flutter, I had heard of Dart, but had never used it myself and had seen old articles like Why do people hate Dart? that made me wonder if it was going to be an advantage or drawback of the framework. If you’re interested in learning more about why the Flutter team chose to use Dart, you can read about their decision here.

Since writing Flutter means writing a lot of Dart code, I wanted to share some of my takeaways on how Dart creates a productive developer experience:

  1. No Surprises
    One of the design principles when Dart was launched in 2011 by Google was to make it feel familiar and natural to programmers and thus easy to learn.

    Skimming through a tour of the language, a lot if it indeed looks familiar to a developer with a background in any object-oriented language:

     void main(List<String> arguments) {
       print(arguments);
     }
    
     const list = ['apples', 'bananas', 'oranges'];
    
     list.forEach((item) {
       print('$item');
     });
    
     var nobleGases = {
       2: 'helium',
       10: 'neon',
       18: 'argon',
     };
    
     class Point {
       double? x; // Declare instance variable x, initially null.
       double? y; // Declare y, initially null.
       double z = 0; // Declare z, initially 0.
     }
    

    I started writing Flutter code with almost no time spent reading through the Dart language.

    As a best practice that’s not a very smart idea, and I later went back to learn more, but it speaks to how simple the language is to use. All of the basic language features like maps, functions and classes were very familiar and approachable.

    Comparatively, JavaScript is also known for being a great starter language and one that I’ve also used a lot. While its widespread availability and versatility has made it one of the most popular languages in the world, it’s also famous for its many inconsistencies.

    You can do some really powerful things with JavaScript that are not possible in Dart and other typed languages, but it sometimes comes with the disadvantage of making it easier to shoot yourself in the foot.

  2. Great Documentation

    Dart makes documentation a priority of the language with support for documentation comments. While Dart shares a double forward slash // for commenting with a number of programming languages, a triple slash /// creates a documentation comment as shown below:

     /// A domesticated South American camelid (Lama glama).
     ///
     /// Andean cultures have used llamas as meat and pack
     /// animals since pre-Hispanic times.
     ///
     /// Just like any other animal, llamas need to eat,
     /// so don't forget to [feed] them some [Food].
     class Llama {
       String? name;
    
       /// Feeds your llama [food].
       ///
       /// The typical llama eats one bale of hay per week.
       void feed(Food food) {
         // ...
       }
    
     /// Exercises your llama with an [activity] for
     /// [timeLimit] minutes.
       void exercise(Activity activity, int timeLimit) {
           // ...
       }
     }
    

    The comments support embedded links to methods and classes and are automatically rendered in editors like VSCode:

    All libraries published to the Dart package manager get free, hosted API references incorporating all of the code’s documentation comments automatically. Here’s an example from one of our own packages.

  3. Community Adoption
    In 2018, the same year that the Why do people hate Dart? thread was featured on Hacker News, Dart wasn’t even listed in the most loved languages section of Stack Overflow’s yearly developer survey.

    In its debut appearance on the list in 2019, Dart came in at number 12, just below JavaScript.

    As of the 2021 survey, Dart has moved up the list to number 6:

    While Flutter has become the 2nd most loved framework and the highest of any cross-platform tool:

    A lot of this momentum comes from the design decisions that Dart and Flutter took to make it easy to use and share across platforms.

So how does Flutter work?

Before Flutter, when I heard cross-platform, my first thought was an app for iOS and Android. That is what I was familiar with from React Native, which launched over 2 years before Flutter’s initial release in 2017. Today, Flutter has expanded its support to additional platforms:

  • Android
  • iOS
  • MacOS
  • Linux
  • Web (2021)
  • Windows (2022)

To be fair, they’re not alone. React Native has had a separate project for React Native Web and in 2020, Microsoft made their own React Native extension for Windows and MacOS.

These are separate projects though, and one of the great things about Flutter is how closely integrated and accessible it makes each platform. In fact, when you first download Flutter’s example project and hit run, it opens on web!

How does Flutter support so many platforms?

Unlike other cross-platform frameworks, Flutter doesn’t use the native components of each platform. Their architecture notes break this down nicely:

Cross-platform frameworks typically work by creating an abstraction layer over the underlying native UI libraries, attempting to smooth out the inconsistencies of each platform representation. App code is often written in an interpreted language like JavaScript, which must in turn interact with the Java-based Android or Objective-C-based iOS system libraries to display UI. All this adds overhead that can be significant, particularly where there is a lot of interaction between the UI and the app logic.

By contrast, Flutter minimizes those abstractions, bypassing the system UI widget libraries in favor of its own widget set. The Dart code that paints Flutter’s visuals is compiled into native code, which uses Skia for rendering. The same is true for Flutter on other native platforms, such as iOS, Windows, or macOS.

Flutter draws all of its own elements on a canvas itself, similarly to how many game engines work. By using the same renderer, framework, and set of widgets across all platforms, Flutter aims to help developers build consistent experiences across platforms with less code.

When it comes time to release a project, Flutter apps are compiled directly to machine code, whether Intel x64 or ARM instructions, or to JavaScript when targeting the web.

Flutter in Production

So is Flutter production ready?

  • Android ✅
  • iOS ✅
  • Web (Yes ✅ and no ❌)
  • Chrome extension ✅

Many companies are using Flutter for native development in the wild including Betterment, BMW, ByteDance and others that you can browse at the Flutter showcase.

I found that the app runs great in production on both native platforms, with the main limitation on quality being my own lackluster design chops 😆. I have not yet worked with Flutter for MacOS or Windows, so I can’t speak to those experiences, although Flutter for Windows recently announced its first production release and availability on the stable channel as of early 2022.

Flutter for Web

What’s the story on web? Flutter similarly announced its first production web release in early 2021, so now almost a year later, where does it stand today?

Our own app uses Flutter web in production and it has allowed us to share a lot of code and speed up development across web and native platforms. Using Flutter web in production, however, will end up being a situational choice on projects for a few main reasons. To understand why, first we need to explore how Flutter web rendering works.

Web Renderering

As mentioned in the architectural breakdown, Flutter does not use native elements to build its UI and instead uses Skia, a 2D graphics library, to provide consistent rendering across all platforms.

On web, Flutter uses CanvasKit, a web assembly implementation of the Skia graphics APIs. This means that when you inspect the DOM of your Flutter web app, you won’t see a deep tree of DOM elements. Instead, you will just see a custom body tag:

Flutter DOM

UI elements are still able to be inspected using Flutter’s own inspector available in editors like VSCode, but you are not able to inspect them yourself in the browser’s developer tools since there is no DOM representation.

This rendering approach comes with a few limitations:

  1. Limited SEO Support

    Since Flutter compiles to a JavaScript SPA (Single Page Application), search engines like Google are not able to easily crawl and rank the app’s pages. Additionally, since Flutter web does not use HTML elements, each page’s content itself is not crawlable since there aren’t any text, link or similar elements.

    You can still specify values for your site’s description, title and other meta fields in the project’s index.html, but that’s it for SEO on Flutter web as it stands today. This issue has been discussed at length on the Flutter GitHub if you would like to learn more.

  2. Large Bundle Size

    The CanvasKit web assembly library currently sits at a hefty >2MB download size, making any Flutter web app already comparable or larger than the bundle size of many production apps. This is especially an issue for mobile web, where network speeds are slower and less reliable.

    If that’s a dealbreaker for your web project, Flutter does come with an another option. An alternative HTML web renderer can be specified when building a Flutter web application, which uses a combination of HTML, CSS, Canvas elements and SVG elements to render your UI.

    While it enables a substantially smaller bundle size, the web renderer still does not generate a crawlable DOM tree for SEO content and does not have as high performance or visual fidelity as the CanvasKit renderer.

    By default, Flutter will automatically select which renderer to use based on your browser, choosing CanvasKit for desktop and HTML for mobile.

The Flutter team acknowledges the limitations of Flutter web and makes the case that it isn’t designed to be the right tool for all web projects. Instead, they suggest using it situationally for app-centric experiences like:

  • Progressive Web Apps
  • Single Page Apps
  • Existing Flutter mobile apps

This can include sites like admin panels, web clients for existing apps like a music player, and other types of experiences where first-time load and SEO discoverability are not as critical.

The door hasn’t been closed on improving either of these issues over time, but for now Flutter will remain a situational choice for web apps. If you’re not sure, give it a try for yourself!

Flutter for Chrome Extensions

One area of web development where Flutter really shines is for building Chrome extensions. A Chrome extension has a similar form factor to a mobile app, making it an easy platform to expand to when starting with a Flutter iOS or Android application.

It also is less important for a Chrome extension to have a small bundle size, since it’s only available on desktop, and does not rely on SEO.

It takes a couple steps to get running with a Chrome extension for Flutter, which if you’re interested, you can read about in another blog post.

Getting Started with Flutter

Flutter has excellent documentation to help developers get started. One of the documents I found most helpful at the beginning was their from another platform guide that walk developers through how to accomplish tasks with Flutter that they are used to doing on other platforms like iOS, Android and React Native.

There are many different areas to focus on when starting with a new framework and language, including state management, authentication, network fetching, deep linking and others. Our app uses FlutterFire, a set of Flutter plugins for Firebase to solve many of these problems including authentication with Firebase Auth, a serverless datastore with Cloud Firestore and deep linking with Firebase Dynamic Links.

For state management, the Flutter docs have a great breakdown on some of the popular commmunity options.

To explore the solutions used in this app, you can check out some of the packages built for it, as well as other blog posts on topics like building widgets by platform and form factor, infinite scroll views, state management and general tips for building cross platform Flutter apps.

A Great Community

The Flutter community is really active and has been a great resource throughout the process of learning and working with Flutter. You can check find all the links to Slack and Discord channels, subreddits and more at the community page and if you see someone with a 💙 in their name on Twitter, chances are you’re dealing with a Flutter fan!

Hopefully this exploration into Flutter has been helpful and if you would like to chat about anything Flutter related or about the project, feel free to reach out on Twitter or at dan@pollyn.app.

Happy coding!

Categories

Flutter Guide