software engineering Sep 19, 2018

Flutter — A Production Ready Checklist

With the release of Beta 3, Google has announced that Flutter — the open-source mobile application development SDK — is production ready. However, many developers might be cautious to bring a feature into production before its stable 1.0 release. So I’ve researched some of the aspects that would be needed for a production-ready app in Flutter and documented them here.

If you are unfamiliar with Flutter or what makes it different from other popular frameworks such as ReactJS, I recommend checking out this post:
What’s Revolutionary about Flutter.

Before We Start

One incredible thing about Flutter is that functionally, it can do anything that a native application can through the use of Platform Channels and Message Passing. This is how the majority of Flutter’s plugins work — Flutter instructs the native iOS/Android code to perform an action and returns the result to Dart.

Flutter Directory Structure Flutter Directory Structure

If you take a look at the directory structure of a Flutter app, you’d notice that along with the lib directory (which contains Dart code), there is also an android and an ios directory. These contain the iOS and Android “hosts”, native projects that will eventually run the compiled Dart. To introduce communication, all that’s needed is to create a Platform Channel, such as MethodChannel, in both the Dart and the host. These channels are well-supported so once they are created, they can send messages and different data types between themselves. The link above details this further.

platform channels diagram Platform Channels can pass data between Flutter and Native code

Because of this capability, many production-ready factors can be accomplished simply by calling native code. This post will not visit all production ready factors. I’ve chosen just a few of the aspects I think are essential for a production-ready app and those that might not be accessible through the use of platform channels.

The Checklist

  • Security
  • Native Integration
  • Multiple Entry Points
  • Testing
  • Localization
  • Accessibility

Security

The most important part of any production application is security. Protecting user information, your back-end network, intellectual property, etc must be a priority for any production application. Let’s delve into how code obfuscation, certificate pinning, and static code analysis work in Flutter.

Code Obfuscation Obfuscation is the practice of making something difficult to understand. Programming code is often obfuscated to protect intellectual property and prevent an attacker from reverse engineering a proprietary software program
Generic Obfuscation Example Generic Obfuscation Example

There are existing tools that obfuscate native Android and iOS applications, e.g. ProGuard for Android. What about Flutter? According to their wiki, obfuscation is supported for Dart code. At this time, the page states, “Note that Dart obfuscation has not yet been thoroughly tested”, which attests to Flutter’s beta status. I’ve documented the steps to add obfuscation below.

Android
Add the following flag to <flutter_project>/android/gradle.properties

extra-gen-snapshot-options=--obfuscate

iOS
In <flutter_sdk_path>/packages/flutter_tools/bin/xcode_backend.sh, add the following flag to the ‘aot’ call

${extra_gen_snapshot_options_or_none}

Define the flag as:

local extra_gen_snapshot_options_or_none=""
if [[ -n "$EXTRA_GEN_SNAPSHOT_OPTIONS" ]]; then
  extra_gen_snapshot_options_or_none="--extra-gen-snapshot-options=$EXTRA_GEN_SNAPSHOT_OPTIONS"
fi

Then in your Release.xcconfig mark the obfuscation flag

EXTRA_GEN_SNAPSHOT_OPTIONS=--obfuscate

I tried the obfuscation for myself. With the obfuscation enabled, the compiled Dart of a Flutter release build (for Android) is bundled into a shared object (.so) file. Unfortunately, decompiling the file looks like it would take professional tools and resources in addition to an understanding of ARM architecture for it to be useful. This might deter many attackers who don’t have the right tools or knowledge, but there wasn’t really a way to verify how well the obfuscation was done.

Certificate Pinning During the handshake that takes place when an SSL/TLS connection is established the client can authenticate the server it is talking to by validating that the server certificate was issued by a Certificate Authority that the client trusts

Certificate pinning isn’t supported in Dart. There is a third-party plugin, ssl_pinning_plugin, which has some functionality but isn’t very comprehensive. It checks if a server’s SHA1 fingerprint matches the fingerprint that is given. Once that is validated, you can make regular network calls assuming the connection is secure.

Looking through a few related issues on Github, Flutter is getting a few steps closer to supporting certificate pinning. Currently, it seems that for a complete solution, the ssl_pinning_plugin would need to add more support or another native plugin would need to be created.

Static Code Analysis Static Code Analysis commonly refers to the running of Static Code Analysis tools that attempt to highlight possible vulnerabilities within ‘static’ (non-running) source code by using techniques such as Taint Analysis and Data Flow Analysis.

Every static code analyzer I’ve seen does not currently support the Dart language since it’s relatively new and doesn’t have widespread use — especially in enterprise environments. One thing that I personally tried was using the web tool dart2js in order to convert a Flutter project into JavaScript. Unfortunately it failed to run correctly, mostly because it doesn’t like the Flutter package.

> dart2js -o main.js main.dart
...
flutter/packages/flutter/lib/src/semantics/semantics.dart:2921:12:
Error: Not a compile-time constant.
case TextDirection.ltr:
^^^^^^^^^^^^^^^^^
flutter/packages/flutter/lib/src/semantics/semantics.dart:2921:12:
Error: Not a compile-time constant.
case TextDirection.ltr:
^^^^^^^^^^^^^^^^^
Hint: 1132 warning(s) suppressed in package:flutter.
Hint: 1 warning(s) suppressed in dart:core.
Error: Compilation failed.

I also successfully tested it on the simple Dart program found below (with no Flutter). However, the outputted JavaScript file was longer than expected, a couple hundred lines when the Dart program was only three! I suspect that it also includes a few Dart web packages. So even if there was Flutter support for dart2js, the output wouldn’t be quite useful for a static code analyzer due to irrelevant code.

// Simple Dart Program
main() {
  print("Hello World");
}
// First lines of transpiled js
(function() {
  // /* ::norenaming:: */
  var supportsDirectProtoAccess = function() {
    var cls = function() {
    };
    cls.prototype = {p: {}};
    var object = new cls();
    if (!(object.__proto__ && object.__proto__.p === 
     cls.prototype.p))
      return false;
...

Native Integration

Take the case where you already have existing native Android and iOS apps that you want to add the same new feature to. A quick and common cross-platform solution would be to embed a WebView within the app and write the new feature as a web page. But what if instead we were to create a new feature that was written in Flutter?

This could offer a few benefits. Often, WebViews are not as performant, secure, or nice looking on a mobile platform. Flutter offers to provide that quick cross-platform solution, but with all the speed, security, and aesthetic benefits of a native app!

Native integration does work well! There were two different methods of integrating Flutter that I tested — both were successful and focused on opening an entire Flutter flow rather than embedding it into a native view. I will be documenting these steps in an upcoming post.

Native Integration Android / iOS Native Integration Android / iOS

Multiple Entry Points

As a natural progression to native integration, what if we wanted multiple different Flutter features in our native app? It turns out this isn’t currently a supported use case because there is no way to reference what Dart code to run. When a Flutter application is launched, it will always launch main.dart.

It’s definitely possible to simulate this type of behavior by structuring main.dart to render dynamically. All that needs to be done is pass some kind of key from the native app (through a MethodChannel) when it initializes the Flutter flow.

In the example below, main.dart waits for the native app to provide the getEntryPoint key, then it switches on the key to render either App1 or App2.

Flutter Android animation
// main.dart
void main() {
  handleEntry();
}
Future<Null> handleEntry() async {
  const entryPlatform = const MethodChannel('com.example/Entry');
  try {
    final String result = await entryPlatform.invokeMethod('getEntryPoint');
    switch (result) {
      case "FlutterEntry1":
        runApp(new App1());
        break;
      case "FlutterEntry2":
      default:
        runApp(new App2());
    }
  } on Exception {
    runApp(new App2());
  }
}

The only downside with this method is that it isn’t very pretty. As more features are added, more keys will need to be tracked in both the native and Flutter portions of the app. In an ideal situation, there wouldn’t be any need to maintain any sort of Dart or Flutter code. Using the integration above, it might simply reference a specific widget to render or maybe even have a main.dart bundled within each widget.

current structure vs preferred structure current structure vs preferred structure

Testing

Flutter provides support for three different types of tests: unit, widget, and integration tests. They are provided through the test and flutter_test packagesFlutter also provides a driver and the ability to mock data through the flutter_driver and mockito packages. To incorporate any of the above, add the following dependencies.

dev_dependencies:
  flutter_test:
    sdk: flutter 
  flutter_driver:
    sdk: flutter

While tests aren’t technically needed for production, the ability to write tests on these three different levels makes code easy to maintain and safer to modify. Existing native solutions for testing such as Espresso or UIAutomatorwill not work because Flutter doesn’t render the UI in the same way as native applications. This is fine because the Flutter widget and integration tests have similar functionality. Examples of different types of tests can be found in the link above.

Localization Localization is the process of adapting a product or content to a specific locale or market

Flutter does support localization! This can be done through the flutter_localizations package which utilizes the device’s locale. Note that changing a devices locale will not automatically change a Flutter application’s language.

dependencies:
  flutter_localizations:
    sdk: flutter

Once a locale is ascertained, translations can be done in two different ways. The first is to simply create a map of the values you would like to store. The second is creating an API with the Dart internalization package intl. A full example of an API can be found here. The following code and GIF shows a simple implementation of the mapping method.

static Map<String, Map<String, String>> localizedValues = {
  'en': {
    'title': 'Hello World',
  },
  'es': {
    'title': 'Hola Mundo',
  },
};
Localization Example Using a Map Localization Example Using a Map

Accessibility

Accessibility is provided through three different semantic Accessibility Widgets. These semantics provide annotations for widgets contained in an app which can be read by an iOS or Android screen reader.

The Semantics widget provides annotations on a single sub-widget while MergeSemantics will describe a group of Semantics. It’s important to note that many of Flutter’s provided widgets are already declared as Semantics, such as the Radio widget found within the example below.

new Semantics(
    container: true,
    button: true,
    label: "A button to tap",
    child: new GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
            onChanged(value);
        },
        child: new Text(
            "Tap Me,
        ),
    ),
)
new MergeSemantics(
    child: new Row(
        children: <Widget>[
            new Radio<_ChoiceValue<T>>(
                value: value,
                groupValue: groupValue,
                onChanged: onChanged,
            ),
            new Expanded(
                child: new Semantics(
                    container: true,
                    button: true,
                    label: value.label,
                    child: new GestureDetector(
                        behavior: HitTestBehavior.opaque,
                        onTap: () {
                            onChanged(value);
                        },
                        child: new Text(
                            value.title,
                            style: theme.textTheme.subhead,
                        ),
                    ),
                ),
            ),
        ]
    ),
),

ExcludeSemantics removes widgets the from the Semantic Tree. This might be used in the case for purely decorative widgets.

new ExcludeSemantics(
    child: new Container(
        alignment: Alignment.topLeft,
        child: new GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: _toggleFrontLayer,
            onVerticalDragUpdate: _handleDragUpdate,
            onVerticalDragEnd: _handleDragEnd,
            child: widget.frontHeading,
        ),
    ),
)

Font Size

Font sizes are automatically calculated by Flutter based on the current operating system. A developer just needs to account for word wrapping and truncation when creating their UI.

Summary

Overall, Flutter does provide many of the production-ready functionalities I’ve listed. Personally, I’d really like to see code obfuscation fully tested and see Dart/Flutter support added to existing static code analyzers.

Over time, Flutter will no doubt become fleshed out with more plugin options that enable practices like certificate pinning. Flutter is definitely something to keep an eye on as Google and the community are clearly taking strides to close these gaps.

Thanks to Nick Capurso.

Phillip Ngo
Capital One intern and student at Virginia Tech

DISCLOSURE STATEMENT: These opinions are those of the author. Unless noted otherwise in this post, Capital One is not affiliated with, nor is it endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are the ownership of their respective owners. This article is © 2018 Capital One.