Hey iOS developers, come take a look, I’ll show you around.
On December 4th, Google launched Flutter 1.0, showing how we will build beautiful user experiences in the future.
I was lucky enough to attend Flutter Live in person. If you missed the event, you can catch up on the amazing announcements here:
Today I share my thoughts on why Flutter is already a superior technology for front-end development, and will be even more so in the future.
Just to be clear: The opinions in this article are entirely my own and not endorsed by Google.
More in detail, we’re going to look at:
- The last 10 years of iPhone history and the Apple ecosystem
- What makes Flutter more productive
- A direct Flutter-iOS comparison by example
This is going to be a long article, so here is the short version:
TL;DR
- Mobile app developers spend a LOT of their time building UIs.
- The development process is much slower on iOS than it is on Flutter.
- In particular, the iOS tooling (Xcode) and APIs are sub-optimal for building modern apps.
- Flutter is far more productive.
- Flutter makes it easier to do things that are really hard on native SDKs.
- Flutter will enable full cross-platform development across iOS, Android, desktop, web and beyond with one codebase.
- Developers and entire product teams will ship better products, faster.
- This will greatly benefit small independent shops, startups and digital agencies.
- Apple could build better tools and APIs, but these would remain confined to their ecosystem.
- Remember: Apple is a hardware company, and has no incentive in building and promoting a cross-platform framework.
Exactly how we got to this point is a fascinating story.
Having been an iOS developer since 2012, I’m familiar with the ins and outs of building products for Apple platforms.
And having experienced first-hand how Flutter is radically different, I want to share my perspective.
Starting from the beginning.
The iPhone
This device changed everything. It really did.
And Apple did a superb job at creating the right experience on this new Internet device.
With the iPhone, every app was just an icon on your home screen. Multitasking wasn’t supported until iOS 4.0.
To make up for that, a child could quickly learn the basic interaction patterns.
The device was sexy, and easy to use. And Apple’s marketing machine helped making the iPhone a resounding success.
Naturally, developers jumped at the opportunity, and started building apps with the available iOS tools and APIs.
UIKit was the framework used for building UIs on iOS, and it was originally designed for the first iPhone.
But then, new screen sizes came, and then the iPad.
And suddenly, new considerations had to be made about how to position content on screen.
And these had to baked into UIKit in the form of new APIs. With springs & struts first, and then auto-layout.
And iOS developers went along with these changes as they were introduced.
It wasn’t the smoothest ride ever:
Awesome Auto Layout fail! pic.twitter.com/RhvOWFuJud
— Janie Larson (@RedQueenCoder) December 17, 2015
Alongside working with UIKit and its legacy, iOS developers had to contend with a 34 year old language called Objective-C (so old, in fact, that it didn’t have a logo!). This would use manual reference counting to manage memory, until the introduction of ARC in 2011.
But many of us had a love affair with iOS development.
I remember building and launching my first iOS app. It made it possible to combine touch interaction, camera input, 3D graphics, and UI overlays.
Back then, I was in wonder, and I knew my heart would be on iOS for a long time.
Swift
Fast forward to 2014, Swift was launched.
Many of us jumped with joy and embraced this shiny new language.
Initially it would crash Xcode. A lot:
Writing Swift. Managed to crash Xcode on Yosemite so hard, even the Beachball stopped spinning. SourceKit used up ALL the memory and crashed
— Peter Steinberger (@steipete) June 24, 2014
But it got better.
And Swift was a very significant departure from the closed-source nature that was typical of Apple.
Chris Lattner (the mastermind behind Swift and LLVM), had been working on Swift since 2011, and managed to have it blessed as the future programming language in the Apple ecosystem.
Swift had a huge audience from the start, since it was fully compatible with Objective-C and the existing iOS APIs.
But Swift was meant to grow and expand beyond iOS and the Apple ecosystem.
Using Chris Lattner’s own words:
My goal for Swift has always been and still is total world domination. It’s a modest goal. - Chris Lattner
It was open-sourced in 2016. And it started getting interest on the server-side, but didn’t quite take off in that space.
You see, Swift had been let down. By Apple’s strategy with Xcode.
Developers outside the Apple ecosystem had enjoyed more productive IDEs for years.
And for any given language, they could often choose between multiple IDEs.
But if you wanted to use Swift, you had to use Xcode.
Until October 2018, when Apple announced Language Server Protocol (LSP) support for Swift. This opened up the language for integration with other IDEs, such as Visual Studio Code.
Swift is a great language, and will enjoy much success in the future, even outside the Apple world.
And replacing Objective-C was a problem worth solving for Apple itself and developers alike.
However, this was not the most important problem.
Because mobile apps developers spend A LOT of time writing UI code.
And the current workflow for making apps on iOS (and Android) is… meh.
On top of that, native APIs have accumulated 10 years of legacy code. They were never designed for building modern, reactive applications.
Swift is a great language, but the APIs we use are our bread-and-butter. They are not as good as they could be. And the tooling is too slow.
For me, this was becoming a productivity killer. I started looking outside the Apple ecosystem, and I discovered Flutter.
Flutter
The four core tenets of Flutter is that it is beautiful, fast, productive, and open.
So how is Flutter more productive than traditional iOS development?
For starters, I can’t overstate how hot-restart and sub-second hot-reload have changed my workflow. It is truly game-changing.
But there is a lot more. And this has to do with the way Flutter code is written.
By design, Flutter code is declarative, favours composition, and uses a single threading model.
As we will see, this leads to code that is more concise and easier to reason about.
Declarative code
With Flutter, we declare UIs programmatically by combining widgets to form a widget tree. In fact, the whole app is a widget.
This is the only way of building UIs, because the entire set of widgets in the Flutter SDK uses declarative APIs.
Hence, it is easy to write UI as a function of state. There simply is no other way.
And unless heavy computations are needed, all code runs on a single isolate, resulting in fewer bugs.
Compare that to iOS development, where:
- imperative code is the norm
- we have many different ways of doing things
- code often runs across threads
As an example, just look at communication between objects on iOS. We have closures (callbacks), delegates, notifications, target-selectors, key-value observation.
And these patterns can (and often are) used across all application layers (UI, controllers, models, services).
Given the same requirements, different teams may produce completely different code.
Of course, the same is also true to some extent in Flutter. But it happens well within the boundaries of using declarative UIs.
There is less cognitive overload in understanding the UI code in a Flutter app. This makes it easier to refactor other people’s code, and the tooling for doing so is excellent.
Declarative code means that we are reasoning at a higher level of abstraction, compared to imperative APIs.
And in Flutter we use declarative over imperative code by default.
Let me show you what a difference this makes, with an example.
Example: UITableView
Suppose we want to build a simple page with a list of contacts:
(not pixel perfect, more on this later)
The iOS way
Let’s see how we can build this with a table view on iOS.
First of all, I can use Interface Builder to create a view controller with a UITableView
and a custom cell inside:
With this approach, all the layout constraints are defined in the storyboard.
Then, I can write a simple view controller class to hook things up:
import UIKit
struct Contact {
let name: String
let email: String
}
class ContactTileAvatar: UIView {
@IBOutlet var label: UILabel!
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.size.width / 2.0
}
var name: String? {
didSet {
if let name = name, name.count > 0 {
label.text = String(name.prefix(1))
}
}
}
}
class ContactCell: UITableViewCell {
@IBOutlet var leading: ContactTileAvatar!
@IBOutlet var title: UILabel!
@IBOutlet var subtitle: UILabel!
var contact: Contact? {
didSet {
title.text = contact?.name
subtitle.text = contact?.email
leading.name = contact?.name
}
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allContacts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactCell else {
fatalError("cell not registered")
}
cell.contact = allContacts[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 73.0
}
}
let allContacts = [
Contact(name: "Isa Tusa", email: "isa.tusa@me.com"),
Contact(name: "Racquel Ricciardi", email: "racquel.ricciardi@me.com"),
Contact(name: "Teresita Mccubbin", email: "teresita.mccubbin@me.com"),
Contact(name: "Rhoda Hassinger", email: "rhoda.hassinger@me.com"),
Contact(name: "Carson Cupps", email: "carson.cupps@me.com"),
Contact(name: "Devora Nantz", email: "devora.nantz@me.com"),
Contact(name: "Tyisha Primus", email: "tyisha.primus@me.com"),
Contact(name: "Muriel Lewellyn", email: "muriel.lewellyn@me.com"),
Contact(name: "Hunter Giraud", email: "hunter.giraud@me.com"),
]
A few things to note:
- I have defined a
ContactTileAvatar
class so that I can have a custom circular shape. - I have a
ContactCell
subclass ofUITableViewCell
, which takes aContact
model. - The view controller itself implements the required methods in
UITableViewDelegate
andUITableViewDataSource
.
The Flutter way
In Flutter, there is no Interface Builder and we can build our page entirely in code:
import 'package:flutter/material.dart';
class Contact {
Contact({this.name, this.email});
final String name;
final String email;
}
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
CustomAppBar({this.title = ''});
final String title;
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(
title,
style: TextStyle(
fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black),
),
backgroundColor: Color(0xFFF9F9F9),
elevation: 0.0,
bottom: PreferredSize(
child: Divider(height: 0.5, color: Colors.black26),
preferredSize: Size(double.infinity, 0.5),
),
);
}
Size get preferredSize => Size.fromHeight(44.0);
}
class ContactListTile extends ListTile {
ContactListTile(Contact contact)
: super(
title: Text(contact.name),
subtitle: Text(contact.email),
leading: CircleAvatar(child: Text(contact.name[0])),
);
}
class ContactsListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'List'),
body: _buildList(),
);
}
Widget _buildList() {
return ListView.separated(
itemCount: allContacts.length,
separatorBuilder: (context, index) {
return Padding(
padding: EdgeInsets.only(left: 16.0),
child: Divider(height: 0.5, color: Colors.black26),
);
},
itemBuilder: (content, index) {
Contact contact = allContacts[index];
return ContactListTile(contact);
},
);
}
}
List<Contact> allContacts = [
Contact(name: 'Isa Tusa', email: 'isa.tusa@me.com'),
Contact(name: 'Racquel Ricciardi', email: 'racquel.ricciardi@me.com'),
Contact(name: 'Teresita Mccubbin', email: 'teresita.mccubbin@me.com'),
Contact(name: 'Rhoda Hassinger', email: 'rhoda.hassinger@me.com'),
Contact(name: 'Carson Cupps', email: 'carson.cupps@me.com'),
Contact(name: 'Devora Nantz', email: 'devora.nantz@me.com'),
Contact(name: 'Tyisha Primus', email: 'tyisha.primus@me.com'),
Contact(name: 'Muriel Lewellyn', email: 'muriel.lewellyn@me.com'),
Contact(name: 'Hunter Giraud', email: 'hunter.giraud@me.com'),
];
A few notes:
- I define a
CustomAppBar
class to replicate the look and feel of the iOS navigation bar. - The code for showing the list is defined in the
_buildList()
method. - I define a
ContactListTile
, which has the same role as theContactCell
class on iOS.
Going deeper
The two implementations are radically different. Both result in roughly the same amount of code, however:
- On iOS, I define all the layout in IB. I could have written (imperative) auto-layout code to add constraints programmatically. But writing multiple constraints for each view (16 for the
ContactCell
class) would result in a lot more code. - In Flutter, building layouts in code is just a simple process of composing widgets as needed. This is accomplished with fewer lines.
- On Flutter, I extend a
ListTile
widget that already offers all I need to define a table row with an avatar, a title and a subtitle. - On Flutter, all I need to do to define a “table view” is to use a
ListView
widget. - This comes with a convenient
ListView.separated
factory method. This makes it easy to provide a separator, and a “tile” for each row via abuilder
callback. - This is in stark contrast with the delegate-based approach used on iOS.
- On iOS, we need to do the
dequeueReusableCell
dance withguard let
and optional down-casting. - On Flutter, I define a
CustomAppBar
. This is an extra step I made to implement an iOS-style navigation bar. This class is written once and can be reused in all screens with a navigation bar.
Of course, the iOS table view boilerplate code could be abstracted away with a cleaner implementation that uses generics.
But this is extra work, and it highlights the drawbacks of working with old imperative APIs.
It took me considerably longer to build this example on iOS than it did on Flutter.
Looking at this with the eyes of a beginner, I feel that learning how to use table views is hard on iOS. And still quite time consuming even as an experienced developer.
So Flutter is not only more productive, but easier to learn as well.
Composition vs addition
By the way, I had a peek at the documentation for UITableViewDelegate
. It contains 40 different methods as of iOS 12. Forty. Talk about single responsibility principle.
This boils down to how UIKit has evolved over the years. And the same pattern applies elsewhere (I’m looking at you, UIApplicationDelegate
).
Compare that to Flutter, where composition is king. Yet one more design choice that leads to more modular and flexible APIs.
Is Flutter on iOS pixel perfect?
Look closely at the previous example:
The kerning (space between characters) in the titles is slightly different, and the navigation bar itself may be 0.5 points taller.
But you know what? We can fix this very easily with a couple of tweaks to TextStyle
and preferredSize
in our Flutter code above.
You see, I could have chosen to use Cupertino widgets for this example.
But I didn’t. Instead, I created a CustomAppBar
class out of simple widgets to get the look I wanted. That took me about 10 minutes, because:
Flutter really shines when it comes to building layouts.
So is Flutter on iOS pixel perfect?
Well, you can make it so if you want.
For me (and a lot of designers I talked to) design consistency across platforms is more important. And with Flutter, it is still possible to embrace platform-specific conventions if desired.
Ultimately, I think a better question is: What matters most?
And my personal answer is shipping high quality apps in a fraction of the time.
Multi-platform workflows
Flutter Live made one thing clear: Flutter is positioning itself as the best tool for building UIs (and beyond) across all platforms.
I have been using Flutter for nine months on a commercial project, and on many open source projects for my blog and video channel.
And I believe Flutter is the holy-grail of front-end development.
I had many pairing sessions with a UI designer who was always amazed at the increased speed of iteration. He even started making UI changes to the codebase himself.
And I worked alongside a backend developer who was writing cloud functions for Firebase. Again, we would build things collaboratively, in realtime.
Our testers were happy to find that the app would perform predictably and consistently on iOS and Android.
This is expected with a product built from a single codebase.
There were differences on some platform specific aspects (e.g. camera permissions), but these were the exception rather than the norm.
What about Apple?
Apple will continue to provide first-class support for their own platforms. That’s been their narrative all along.
They will continue to improve iOS, and maybe even bring iOS and macOS closer together.
They will enhance ARKit and CoreML to support their newest hardware. They need this in part to justify ever-increasing prices in their product line.
But I can’t see them ever providing cross-vendor development tools. Which is what teams and businesses really want.
Ironically, Apple may even stop supporting older devices sooner than Flutter does, since Flutter runs on iOS 8 and above.
What about the Flutter jobs?
There have been very few open positions for Flutter developers up to this point. And people have asked me where are all the Flutter jobs.
I think a lot of companies were waiting for version 1.0. And I have no doubt that the jobs will come.
This is the most exciting time to become a Flutter early adopter.
- Compared to iOS or Android development, the learning curve is not so steep.
- Dart is an easy language to learn, and the tooling is great.
- The official documentation and training material from the community is amazing.
- Flutter is open source and the Flutter Team is very responsive in fixing things.
Native iOS and Android development are here to stay. Flutter apps will continue interfacing to native APIs.
But Flutter is far more productive than native APIs when it comes to pushing pixels around. And makes it possible to write business logic in a single codebase.
This means that if you learn Flutter now, you will have a competitive advantage over developers and teams that stick with existing native workflows.
And this will be a strong proposition when choosing which framework to use for future projects.
In conclusion my advice is: don’t take my word for it, and try it out for yourself.
And I can promise you, Flutter is fun!
So why not taking it for a ride?
Happy coding!
UPDATE: This article was originally called: “Flutter will change everything, and Apple won’t do anything about it”. This has caused some confusion. In the spirit of encouraging and welcoming iOS developers to try Flutter, I have renamed it.
By the way, I published the source code for the example here on GitHub.
And if you want to learn more about layouts in Flutter, here are my top two videos on YouTube:
LEARN FLUTTER TODAY
Sign up for updates and get my free Flutter Layout Cheat Sheet.