Written with Kevin Johnson
As with any coming of age company, Policygenius is looking to deliver not only feature-rich applications but also make sure they are delivered in a reliable and efficient way. Recently we took a look at how we do our Continuous Integration/Delivery. Part of any CI/CD flow is feature flagging, which allows a release of unfinished features behind a flag. Once the feature is ready, the flag is flipped and end users can access the new feature. There are many other uses for feature flags, but this is not the focus of this article.
During our research on which flagging tool we should use, we ended up with two candidates: LaunchDarkly and Optimizely Rollouts. They both offer similar features, but Rollouts has a free tier. While there are official SDKs for the most commonly used languages, we were unable to find the official SDK for Flutter. We use the Flutter framework from Google for building our Policygenius mobile app. You can learn about our selection of flutter over other frameworks in this article we wrote.
Both of these feature flag providers offer official SDKs for iOS and Android, and there is a way to access native code from a Flutter application. In this article we will dive into the details on how we implemented Optimizely rollouts native SDKs with our POC flutter application.
In the Flutter application, native code can be accessed via the MethodChannel class. Basically it is a bridge between Flutter and native code. On one side there is a native receiver which expects messages and routes them to appropriate native methods. And on the other side there is Dart code that creates an instance of MethodChannel and passes a message into it. The whole thing is created as a flutter plugin which allows sharing this code between multiple applications.
For the proof-of-concept application we decided to implement just two main features of the SDK: isFeatureEnabled and getAllFeatureVariables.
1. Run the following command in the directory of your choice:
$ flutter create — org com.example — template=plugin — platforms=android,ios -a kotlin -i swift optimizely_plugi
This will create a Flutter project with the following structure:
lib/optimizely_plugin.dart is one end of the bridge and android/src/main/kotlin/com/example/optimizely_plugin/OptimizelyPlugin.kt OR ios/Classes/SwiftOptimizelyPlugin.swift is the other. This is where we will write most (if not all) of our code.
2. Prepare Flutter code
We will start by implementing the Flutter code first which in turn will call the native code via MethodChannel. Before we can use any Optimizely functionality we need to instantiate the Optimizely manager by passing an sdk key to it. This should be done once on application start. Once the manager has been instantiated we will be able to query Optimizely features. The native SDKs have a lot of methods that we can use, but we will implement just two of them: IsFeatureEnabled and GetAllFeatureVariables. IsFeatureEnabled method should always be called first. If it returns true then we can proceed to calling other methods for the same feature, for example GetAllFeatureVariables in our case.
Add the following code to lib/optimizely_plugin.dart:
If you try calling the code above now, it will throw a PlatformException because we still missing the code on the other side of the bridge. In fact if something goes wrong on the native side, the application will throw this exception. It is a good idea to wrap any code that calls the methods above with try/catch.
3. Import the Optimizely native SDK into your project:
Add the following line to android/build.gradle
- the logger is needed by Optimizely SDK
Inside optimizely_plugin.podspec we need to mark OptimizelySwiftSDK as a dependency by adding this line:
s.dependency = ‘OptimizelySwiftSDK’
When using this plugin and calling flutter run, during the pod install step the SDK will be added in addition to the code we’re going to write.
Now we need to write native code to handle messages that are sent to it via MethodChannel. Let’s start with Android first:
4. Implement native code
Now we need to write native code to handle messages that are sent to it via MethodChannel.
When initializing the Optimizely manager we need to pass the application context to the SDK. We can retrieve this context from the Activity in which the Flutter application is running. In order to do this, our OptimizelyPlugin class (android/src/main/kotlin/com/policygenius/optimizely_plugin/OptimizelyPlugin.kt) also needs to implement ActivityAware interface:
We just need to override one additional method from this interface setting the member variable here:
Now we can create the methods which will interface directly with the Optimizely SDK:
In initOptimizelyManager we are instantiating the manager class and setting it to a local member variable. This needs to be done only once. We can also set how often we would like the manager to pull the updates from the Optimizely server. The minimum interval for Android is 60 seconds. We also initialize the Optimizely client here and instruct it to download updates to cache and reload the client with the newest data, as opposed to waiting for updated flags till we reinitialize the SDK (for most cases on app restart).
Speaking of the datafile, you probably noticed that when we initialize the manager we also pass the content of datafile. A sample datafile can be found here. The actual datafile will be downloaded by the manager from https://cdn.optimizely.com/datafiles/<your SDK key>.json. This is needed in case there is no cached file available:
And finally we need to implement onMethodCall, the entry point for all method channel calls:
Now we will take a look at how we can implement the same for iOS.
First we need to fill in the register method in our Swift plugin file (ios/Classes/SwiftOptimizelyPlugin.swift) so we will receive method calls on the specified channel:
Now for the actual method handling we will implement the handle method below. In the Flutter implementation the methods need to be called on this channel, with these method names and arguments to work correctly.
We are grabbing arguments from the call (ex: userId, featureKey) in order to get the correct data from the Optimizely iOS SDK client, and then return the correct result back to our Flutter application:
Finally here is a breakdown for some of the helper functions called above. getFeatureItems provides items we need to return results from the client and helps avoid some duplication:
Here are extensions for checking if a key exists, the value is the correct type, and helpful FlutterError generation with static methods. This helps by returning useful error messages when debugging issues with missing/invalid arguments.
One of the drawbacks of the plugin approach is that it will not work for the web. For example if you wanted to compile your Flutter application that uses this plugin for the web, you would have to find another solution. As Flutter gets more and more popular, it is our hope that we get an official Optimizely SDK. In the meantime, the plugin solution works fine. For now we only implemented the most commonly used methods inside Policygenius, but we welcome contributions to achieve full parity with native SDKs. Our repo is located here.
Good news — Policygenius is hiring! To find out how you can help us get people the financial protection they need (and feel good about it), check out our open roles.