Porting a 75,000 line native iOS app to Flutter
Not much has been written about porting large apps to Flutter. When I did it I was surprised at the results.
In Australia there’s a native iOS app called Easy Diet Diary.
- has been downloaded 1.2 million times.
- is written in Objective C and Swift and the backend is Amazon AWS.
- contains 75,000 lines of code as per cloc.
For a long time the small company I work for has had an Android version near the top of its To Do list but we didn’t do it because:
- supporting two code bases was too expensive and difficult to manage.
- the main cross-platform development options, Xamarin and React Native, had deal breaking drawbacks (that’s another story).
It’s fast and holds out the promise of a UI experience that for all intents and purposes is indistinguishable from a native app (especially with the iOS widgets added in Preview 2 September 2018).
Here’s what I found broken down into:
- Lines of Code & Development Speed
- What’s Missing?
Lines of Code & Development Speed
When I embarked on the port I estimated it would take 6 man months. Well, the project is coming in ahead of schedule! For me, that’s fairly unusual.😀
In Flutter, instead of using Storyboards (iOS) or layouts in XML (Android) you create an app’s user interface entirely in code by compositing widgets in a widget tree. That sounded a bit scary to me but I jumped in and although it took some time to get used to, before long I was wielding widget trees with some dexterity.
And then, about three months into the port, a curious thing started happening. As I became more proficient and was porting functionality more quickly, the number of lines of code in the project began increasing less quickly. This was strange because I was porting a fair amount of business logic that had an almost one to one ratio in terms of lines of code.
What was happening was that I was able to create classes and write functions to reuse the UI portions of the code much more easily than in native iOS. Often, I could make my UI widgets reusable by simply refactoring them to use a couple of extra parameters and bingo, task done. If that didn’t work I could often simply wrap another widget around an existing widget to accomplish the desired behaviour. It was very satisfying!
In the final release I expect the line count to be less than 30,000 (compared to 75,000 for the native iOS version). Of course, the native iOS version contains code that was developed along the way but ended up being superseded or not being used. I estimate unused code to account for 15,000 lines. In other words 60,000 lines needed to be ported.
So, to summarise, the Flutter port will have half the lines of code of the native iOS original!
Additionally, the Flutter project doesn’t contain any Storyboard XML. And there’s a lot of XML in those Storyboards. The native iOS project contained:
- 15 Storyboards
- 47 nib files
- 92 View Controllers
After struggling with Storyboards for years, trying to follow best practices, building UIs in Flutter is liberating and fast.
I hadn’t realised what a huge proportion of my time was spent writing UI related code and futzing around with storyboards and auto layout constraints. I’ve heard many times, and tacitly agreed, that separating the UI layout from your code is desirable, but that hasn’t been my experience with Flutter. And it’s not just native iOS best practices that promote this separation of concerns concept with regard to the UI. I remember wrestling with XML in Microsoft WPF and see this Quora question Why does Android use XML to define user UI and not just Java code?
Storyboards are a top down way of composing a layout (good for desktop applications) while widgets take a bottom up approach which is a big simplification when you’re building a mobile app. I’ve read that this approach is similar to the way screens are laid out in React Native or with CSS flex-boxes. This article What’s Revolutionary about Flutter by Wm Leler in Hackernoon does a great job of explaining this stuff.
Composing the UI out of widgets was helped a great deal by:
- Flutter’s stateful “Hot Reload” feature which, most of the time, is able to incorporate code changes seamlessly into your running app within a couple of seconds.
- An Android Studio shortcut Alt + Enter to insert or remove UI widgets (rows, columns, containers). See Tips for using Android Studio to develop Flutter apps. I assume there’s something similar in VS Code. This seems trivial but I can’t emphasise enough how useful this shortcut is. With Hot Reload and Alt + Enter I can whip up a screen in minutes or whimsically experiment with a UI.
- Setting debugPaintSizeEnabled to true. This displays brightly coloured borders around all the UI widgets. I thought I’d need this more than I did but it’s invaluable when a layout problem does occur.
For me, choosing an architecture involved:
- Watching this talk by Brian Egan Keep it Simple, State: Architecture for Flutter Apps (DartConf 2018) several times.
- Reading this article by Eric Windmill Using Flutter Inherited Widgets Effectively several times.
- Checking out these Flutter architecture Samples.
I ended up following Eric’s advice and using an architecture pattern called Lifting State Up which is a first step along the way to Redux which looks quite seductive. Redux was, however, a bridge too far for me. With a tight development schedule, learning and experimenting with Redux just seemed too daunting.
Along the way I was helped by excellent answers to architecture related questions written by these prolific Stack Overflowers: Günter Zöchbauer, Rémi Rousselet and Collin Jackson among others. This brings me to community.
The open source community involved with Flutter is very diverse, very supportive and gives me hope for the human race (especially during these crazy times).
As an example, I was using the flutter_slidable package, by Romain Rastel, and suggested an enhancement and within 48 hours he’d implemented a better solution than I had envisaged …and that experience was not unusual.
My only regret is that I’ve been so busy porting that I’ve haven’t given back as much as I’ve received.
Overall, users doing internal testing were happily surprised at how ‘snappy’ the Flutter port was. No noticeable degradation in performance was reported when running the native iOS app along side the Flutter app on the same iOS device.
The app doesn’t do a lot of graphical heavy lifting but one piece of functionality where the Flutter app was noticeably faster involved reading data from hundreds of JSON files and then doing a bunch of floating point calculations on that data. I didn’t take the time to find out whether the difference was in the file reading functions or the JSON parsing or the date handling or something else so I can’t make categorical claims but I felt it was a nice feather in Flutter’s cap …wing? 😃
Looking forward to seeing what kind of benchmark data other people come up with.
Flutter uses Dart, a language that up until the recent surge of interest in Flutter, had languished outside of Google.
It is however mature and easy to learn and I was lucky enough to come along just as the more strongly typed Dart 2.0 hit the streets and you no longer have to constantly pepper your code with the “new” keyword.
Dart may not have the nullable/non-nullable types that Swift and Kotlin have but I have come to love its simplicity. For example:
- Using a leading underscore ‘_’ makes a function private.
- Automatic formatting means one less headache to worry about …and I like using a trailing comma to put parameters on separate lines.
- Package management is simple.
I could go on. I like these features, others may not. Again, the kicker for me is that I can implement functionality much faster (and strangely, more enjoyably) than I have in a long time.
Not too much.
In the native iOS app:
- I used half a dozen targets in XCode combined with #defines to create different variations of the app with different bundle IDs. I’m not sure how I’ll go about doing this in Flutter.
- I used a good third party logging framework in iOS called CocoaLumberjack. Haven’t found a replacement yet.
- I used the AVFoundation framework to combine scanning barcodes and QR codes and taking photos within a single non full screen view. I haven’t got the same fine grained control in Flutter although the camera package is good for photos and, separately, facundomedica has written a great scanning package, fast_qr_reader_view. On Android devices this package hooks up Firebase’s ML Kit to realtime images from the Camera package.
If you’ve read this far you won’t be surprised that I give Flutter a pretty resounding thumbs up.
- The Flutter UI is virtually indistinguishable from native Android and native iOS UIs.
- Because of the way Flutter UIs are constructed I’m able to build new functionality in Flutter more quickly than in native code.
- Testers have not reported any performance degradation when using the ported app.
- The code base shared between the iOS and Android versions of the app is currently greater than 90%. I have yet to integrate Apple HealthKit or Google Fit but don’t expect that percentage to drop too much.
And while Flutter is immature (only production ready since May 2018) it’s been robust for me. The only problem I’ve encountered is when the Dart function to synchronously read a directory, ListSync, broke in a recent release. I added the issue to the Flutter repository on Github and switched to the asynchronous version.
Any feedback is welcome 😃.
If you enjoyed this story please 👏.