Flutter – An iOS Engineer’s Experience
Pros, Cons & Tips for this New Cross Platform Framework
Latest posts by Ciprian Savastre
- Flutter – An iOS Engineer’s Experience - May 1, 2019
There is a new cross platform framework out there which has been gaining more and more popularity.
What is Flutter?
Flutter is a cross platform framework that is open source and promoted by Google. The framework is built with C, C++, Dart and Skia (2D rendering engine). Compared to other cross platform frameworks out there, which use system components for drawing the UI (React Native, Xamarin), Flutter uses OpenGL to render the UI from scratch, and tries to mimic the native experience as much as possible.
Based on documentation it supports Android and iOS, it does not support tvOS and watchOS since it does not support bitcode. It may support Android wear, but there is no official statement about this, just some guys playing around with it. Also note that Fuchsia, the new operating system that Google is working on, has it’s UI built with Flutter.
There is also a project for building Flutter apps for desktop. I saw it in action and it’s pretty cool.
Read on to learn about my experience with Flutter, and discover some noteworthy observations, tips, pros and cons about working with the framework.
My Experience with Flutter
I am primarily an iOS engineer, and almost one year ago my team and I started the integration with Flutter in a project already having iOS and Android apps with 10k+ daily active users.
There was some pain in the process (especially on the iOS side), but we succeeded, and we were able to submit to both stores brand new features implemented entirely with Flutter.
In this project, the Android team even decided to stop doing native development and started porting some of the existing features to Flutter.
In addition, I also worked on some pet projects, implementing a cross platform app using Flutter. On this project I was also able to touch some areas where it needed access to native (iOS/Android) resources or third parties, by creating plugins.
Please note that this is my personal experience, and it may be subjective. If you were to ask an Android engineer from the project I worked on or engineers who implemented the entire app with Flutter from scratch, their opinions may be different.
Noteworthy Observations, Tips & Tricks
Below I’ve listed some of the important things about Flutter that I consider worth mentioning.
- It does not need an interpreter. The framework code is compiled with llvm (iOS) or Android NDK. The Dart code (from SDK or yours) is compiled ahead of time (AOT) into a library, the whole thing gets into a Runner project, and the end result is platform specific which ends up in an ipa (iOS) or apk (Android). For debug mode only, Dart runs in a VM.
- Everything is a widget. Flutter creates a tree hierarchy of the UI using StateFullWidget and StatelessWidget, which are the super class that widgets inherit from. There is no storyboard (iOS) or xml (Android) equivalent, all the UI is from code, and I found it very easy to follow and maintain. Using IntelliJ IDEA you can see the entire Widget hierarchy while debugging. There are plenty of widgets out there which help you build pest UX, take a look on the widget catalog here. One may say that there are too many widgets, which may take some time to get used to, but on the other hand you can get your work done with just a few of them.
- Flutter is reactive. Widgets are stateless or stateful. StatelessWidget never changes (for example a Text displaying a price). On the other hand StatefulWidget changes during the lifetime of the widget (the UI changes based on some user intervention, example a Slider changing its position on user action). StateFulWidget gets redrawn whenever the state changes. You can implement your own widgets and apply the above from a simple control to an entire screen.
- Design patterns. There are plenty of design patterns that work great with flutter. I’ve tried Redux and is awesome for big apps that have a lot of business logic, but it’s overkill for small apps. There are a lot of other design patterns worth trying, but keep in mind that the framework is yet young, and for now apart from Redux, I do not see any trend on which one to prefer (you can find some pointers here).
- Hot reload. With hot reload you can apply the changes that you made in the code in a few seconds. I found this to be a big deal especially when making changes to the UI. It works by injecting the source code into Dart VM and reloading the widget tree. There are some situations where it stopped working. This happens when you open the app, launch a Flutter screen, edit some Dart code, use hot reload, exit Flutter screen and go back to native, and then open the same Flutter screen again. At this step the changes you made on hot reload are not available, so you need to restart debugging.
- Flutter has a nice plugin system (similar to Cocoapods or Carthage, but officially supported), and there are already some plugins which contain some of the things you may want: official plugins, developed and maintained by Flutter team, or third parties maintained by community (check here). However you can get quite easy in a situation where you need some functionality that does not yet exist.
- Developing your own plugins. Whenever you get into a situation where the existing plugins are not enough you can start developing your own plugin. That happens usually whenever there are APIs that each platform handles individually, and you need to access that feature from Dart code. For example if you have proprietary analytics libraries that are different for each platform, and you need to also send analytics from Dart code, you will need a plugin to unify them.Another example would be if an existing plugin does not fit your needs. For example if you want a video player that is capable of streaming over FTP, video_player plugin will not work for you (this plugin is a wrapper over ExoPlayer for Android and AVFoundation for iOS). In this situation you may want to develop your own plugin which will be probably a wrapper over VLC library.
- Binary size. I have created a brand new app from a template, and built the release binaries for both Android and iOS. For Android the apk was 4.9 Mb, for iOS the .ipa was 10.2 Mb, that is pretty close to what you can find in Flutter FAQ as well.
- Toolling. I used IntelliJ IDEA and Visual Studio Code. I have first started with Visual Studio Code, but switched to IntelliJ IDEA, since it was better for debugging.
- Documentation, community and support is pretty good. I rarely encountered an issue which I couldn’t find an answer. On the other hand, there were some issues while it was still in beta, where the turnover was not that good.
Who is Using Flutter?
There are plenty of apps out there using Flutter, but there are also apps that did not make this thing official.
Here are some of some of the apps that appear on Flutter website, and are using this framework as we speak: Alibaba (50,000,000+ users), Google Ads, Hamilton Musical, and others. For more, take a look here or here.
How We Started
As many other teams out there, we were looking for ways to have faster development cycles, single codebase, to implement features faster with fewer engineering resources.
The frameworks that we took into consideration were:
- ReactNative: At the time we were evaluating it, ReactNative license agreement was not compatible for our project, therefore we immediately dropped this idea.
- Flutter: “Wait, but this was still beta,” you might say. Yes, but our Android Team Lead had experience with Flutter before, which was an obvious advantage on using it.
With these considerations in mind, we chose Flutter.
The plan was that we would start with some simple feature, create a POC and distribute it internally in the company, grab some feedback, statistics and decide if it was good for us or not.
We ended up using Flutter for a big feature, and once it was close to wrapping it up, the decision was that we should push to store, have the feature under a feature flag, being available to a certain pool of users.
Integration into Existing Apps
This is where, in my opinion, flutter draws you back. The documentation and the ways of working changed a couple of times over the past year. Even now, after Flutter was officially released, if you check flutter repo, you will find some wiki page about integrating into existing apps, referring to some experimental project Add-to-App, which should make the integration seamlessly with existing apps. The major thing that this project aims for is to add flutter as a module to an existing app.
On our project we do not use the current suggestion from the docs. We followed the initial one, and it works just fine (add iOS/Android as dependency to flutter). Probably we need to update the integration, but I wouldn’t rush too much into it, since even when we started, the docs about integration in existing apps were experimental. They are still experimental (with another new approach) even now, after they officially released Flutter back in December.
Let’s dive into some of the important aspects of integrating with flutter.
- Native is added as a dependency for Flutter. Let’s assume that there is a Flutter app called my_flutter_app and you have a git repository. iOS and Android are added as submodules to flutter (my_flutter_app/ios, my_flutter_app/android).
- You need to add new build phases:
a. Build. This builds Flutter.framework (containing Flutter engine), and App.framework (containing dart code compiled)
b. Link above frameworks
c. Embed the above frameworks
d. Thin binary. This step is needed if you want to release to store. It strips out unnecessary architectures, otherwise the submit to store will fail.
- The workspace, target and module need to be named Runner. Flutter scripts will break if you try to do otherwise. We had to think about how to approach this with minimum changes on existing setup. We even had some crashes due to these changes, where xibs could not resolve a class for a custom view.
- The Info.plist needs to be located in a hardcoded path my_flutter_app/ios/Runner/Info.plist. We were able to fix this by doing a symlink to the existing file.
- Build modes. Flutter supports debug and release modes. Flutter has some configuration files located in my_flutter_app/ios/Flutter: Generated.xcconfig, Debug.xcconfig and Release.xcconfig. Generated.xcconfig is the one containing the path to flutter engine, and other Flutter specific environment variables. This file is regenerated every time you run flutter build from terminal. As for Debug.xcconfig and Release.xcconfig, any build configuration that you may have, must inherit from these. Therefore we had to change a lot on the configuration of our projects.
- Product flavors. In most of the projects you probably do not need this, but if you work in a project where you have multiple schemes or build types (ex: enterprise, adhoc, branding, etc), you may need ways for flutter to understand what type of binary to produce. The bad thing about this is that the config name needs to be like: “Flutter build mode + scheme name.” For example, if your scheme is called myEnterpriseScheme and build mode is Release, then you need to have a build configuration called “Release myEnterpriseScheme” (the order does not matter, you just need to fit these keywords in the configuration name). This was for us a major thing, since we had to rethink the configurations and schemes we already had.
- Disable bitcode. Unfortunately flutter does not support bitcode, so keep that in mind if you consider Flutter. As a result Flutter does not support development for watchOS and tvOS yet. It seems to work on Android Wear, however.
- We had to implement platform channels in order to pass business logic between native and Flutter. The weird thing is that while using the url_launcher plugin this was not working on iOS due to an incompatibility with some of our Podfile settings. Having in mind the time pressure we implemented a platform channel specific for opening urls, only for iOS, but never revisited it to see if the plugin is working now.
- Build times are slower. As I mentioned above there are some new build phases that are added to your Build Phases, and one is for building the Dart code. For incremental builds this added a delay of ~1 minute. Now this may not be a problem for small projects, but integrating it in an existing native project, may be an issue for you. We ended up adding a separate target for native, which we use whenever we need to work on native code.
Here are some build timings from our project:
– select debug flutter scheme, clean, build – 130 s
– modify a line somewhere (example: add a new line), build – 61 s
– select debug native scheme, clean, build – 123 s
– modify the same line as above (example: add a new line), build – 8 s
- CI. You can automate the build process using fastlane. We are currently having a full setup that builds, tests, submits to store (using scan, gym, xcodebuild, pilot, etc). The major drawback is that again, the doc says that you need to run flutter build before producing any artefact, therefore the build time increases a lot. You have two options here: either setup fastlane in Flutter repo (the scripts could be shared for ios and android), or have separate scripts for iOS and Android. We decided to go with the latter, because we had separate setups for CI for both platforms anyway, and we wanted to have the scripts customizable enough so we can produce both flutter and native only builds.
- Valid architecture is limited to arm64 and armv7. This did not impact us that much, but we had a production crash due to the fact that Flutter did not have good support for armv7 at the beginning.
After all the integration was done, we decided to submit to store. The release was successful, but there were a couple of issues that we encountered.
First, fastlane pilot lane was failing on upload due to invalid architectures. The reason for this is because during integration we missed that part where the “Thin Binary” build phase should be after “Embed Frameworks” build phase (which was adding the Flutter framework). After we added “Thin Binary” at we were able to submit to store using phased release.
The first crash was on armv7 devices running iOS 9 (check here). This was a flutter issue, and luckily it was fixed on flutter repo, so all we had to do is to change from beta to master channel based on their recommendation. However we were not happy about this “fix,” since master is their development branch.
The second crash was a rendering issue, and based on our investigation were either related to OpenGL rendering while the app was transitioning to or from a background state or rendering while being in a background state. Based on some issues we found in the Flutter repo, they said that the issue should have been fixed on beta channel, and we should switch to it. However, that did not fix the issue.
The most recent crash was due to a regression in their CupertinoAlertDialog related to using locales (check here). Someone found a workaround, therefore we had to submit a hotfix for it.
In fact, currently we are still experiencing Flutter crashes in our app related to rendering, or other crashes that happen to 1-2 users. However we are still good, since Crashlytics reports that for the top builds there are less than 1% of daily active users experiencing crashes (this including native crashes).
These are some things that I like about flutter:
- Faster development time than native. Apart from writing once deploying twice, you become efficient quite quickly.
- You write less code. This may be subjective, but developing one feature with Flutter and Dart can produce fewer source code lines compared with iOS. Also since may apply to Android as well, since most of Flutter adopters are frp, Android.
- Cross platform tooling. I was able to work on the same app from both Windows and Mac with VS Code. The drawback is that on Windows you do not have a simulator, but you can do some of the work on Windows, if this is what you have at hand.
- The resulted UX is high quality and highly customizable
- Flutter is reactive and it works great with Redux
- Hot reload is cool. Imagine developing a screen in an app having a slow build time.
- Aside the crashes we found, no blockers or performance issues were found
- You write tests very fast
- The docs and community are good. You can find an answer whenever you are blocked.
- It is new, cool and promising
Some cons, which again may be subjective:
- Even though it was recently released, the framework needs to mature before considering it stable.
- Was not designed to be embedded in existing native apps. For Android the integration may take less effort compared to iOS, but on iOS this was not fun at all. A lot of technical debt was added which resulted in more time spent on overcoming some limitations of flutter setup.
- Build time increased
- Debugging is annoying when you have mixed code. For example, if you have some logic that is split between native and Flutter, debugging it’s not that fun. You keep switching back and forth between Xcode to IntelliJ. Also if you experience any layout exceptions, the stack trace is not intuitive at all.
- Setup is fragile. Take a look at the integration section.
- CI build time is big. That is because for each artefact we need to run flutter build again.
- User experience does not feel native. This can be immediately noticed if you have mixed screen, native and Flutter (animations, controls, navigation). However, this may not be a problem if your project does not have other native screens.
- Hot reload is buggy if used in a project with mixed code, when you move back and forth between native and Flutter.
What Happened with Our Integration?
After some time had passed, we needed to evaluate if we would continue with flutter or not.
Engineering and management sat together and decided that every engineer should have some contact with Flutter (at least one task). After all this, we should grab some anonymous feedback within the team, and take the decision afterward.
The main two criteria took into consideration were:
- Framework stability. Are there any crashes, memory issues? Is it reliable? Is the integration stable?
- Compatibility. Engineers should enjoy working with Flutter.
Based on the results of this survey the decision was that the iOS would stop adding new features with Flutter, but keep the integration, since we already had features developed with Flutter which were live. On the Android side, the team decided that they would continue using Flutter for adding new features or porting existing native features.
“Ok, now I’m lost. Should I use it or not?” you may say.
Well, that depends. Flutter seems like a great framework and looks promising, and is a great tool for developing cross platform apps, testing new ideas, and MVPs, and I would recommend it to be used when starting new projects.
However, if you need to embed it in existing apps that already have some codebase, or your app is for a big business, then you need to think twice. In my opinion, Flutter was not designed to be integrated into existing apps out there, hence all the issues we encountered along the way.
Latest posts by Ciprian Savastre
- Flutter – An iOS Engineer’s Experience - May 1, 2019