I'm going to let you in on a secret: For years I hated mobile development. I wanted to like it—mobile was the future! It was cool! It was low-power! It was a way to connect with users whose first exposure to computers did not come from traditional desktop platforms! And yet… development was a slow, frustrating experience for me. Instead, I sequestered myself over in the entirely problem-free area of web development and mourned the disappearance of the HTML blink
tag (kidding).
Then, I discovered Flutter, an open source mobile app SDK developed by Google that enables developers to use the same codebase to create mobile apps for iOS and Android.
Once I found Flutter, I found that mobile development could be joyful.
Yes, joyful.
How, you ask? To show you what I mean, let's walk through writing a very simple Flutter app that queries Stack Overflow. As the self-respecting open source developer that you are, I'm sure you want to keep on top of the questions people are asking about your software on Stack Overflow. This app allows you to search for questions about a specific topic.
Lightning-fast development cycle
Traditional compilers are trouble. You know how it goes: You hit "compile," and the next thing you know you're 10 tabs deep into cute kitten photos—and it's lunchtime. Fortunately, when I worked on the web, interpreters and Just In Time (JIT) compilers saved me from my kitten tendencies, making "edit, save, refresh" the name of the game.
Flutter takes this quick development idea one step further: "edit, save." Although it's not a web technology, you can see your changes on your mobile device's screen in less than a second, thanks to Flutter's hot reload.
Typically you get this fast development cycle by using fancy, dynamically typed scripting languages, with the downside of pushing errors to runtime rather than catching them beforehand at compile-time. The second common drawback is their performance is not as zippy as compiled languages. By using Dart as its programming language of choice, Flutter can sidestep both of these issues. Dart has a strong, sound type system that allows you to catch problems before demoing That Part of the Codebase With Less Than Ideal Test Coverage.
Second, Dart has two modes:
- running in "interpreted" mode on the Dart virtual machine, which allows that joyful, hot reload experience, and;
- compiled mode, which compiles your app down to native machine code when you're ready to release your app.
Given these features, Dart is uniquely suited to provide developers with great development and release experiences for Flutter.
Finally, Dart was designed to be easy to learn, so if you've worked with any C-style language like Java, C++, or JavaScript, it will feel familiar.
Cool features, such as Streams and Futures
Time to start coding! Our app will use the Stack Overflow API to look for questions about Flutter that need responses, so that you, the intrepid open source project owner that you are, can help your community by keeping them informed. The simplest way to get that information in Dart is with an asynchronous request:
final url = 'https://api.stackexchange.com/2.2/questions?
order=desc&sort=activity&tagged=flutter&site=stackoverflow';
var result = await http.get(url);
print(result.body);
The result prints out some JSON, looking something like this:
{
"items": [
{
"tags": [
"android",
"ios",
"flutter"
],
"owner": {
"reputation": 1,
...
},
is_answered: false,
"view_count": 1337,
"title": "How to make a pretty Flutter app?"
...
}
In this code snippet, http.get
is returning a Future<Request>
, which means that the result will be available in the future of type (Http)Request
. Even though we're making a round trip to the server, we don't need to pass in a callback; we can just use the await
keyword to wait for a response. Flutter has FutureBuilder
and StreamBuilder
widgets to build corresponding UI components, given the results of a Future
or a Stream
.
A Stream
is just like a Future
, except that it can provide results asynchronously multiple times instead of just once. In our app, we'll create a Stream where we can listen for updates on our Stack Overflow questions of interest. Since the Stack Overflow API doesn't provide push notifications out of the box, we construct our own stream using a StreamController
and add updated Stack Overflow information whenever we get it:
final controller = StreamController<List<String>>();
void refreshQuestions() async {
var result = await http.get(url);
Map decoded = json.decode(result.body);
List items = decoded['items'];
controller.add(items
.where((item) => !item['is_answered'])
.map<String>((item) => item['title'])
.toList());
}
In refreshQuestions
, we make a new call to the Stack Overflow API, then filter the results so we're only looking at questions that have not been answered. From those results, we pull out the titles of the questions to display them in our app.
Flutter conveniently provides a StreamBuilder
widget that can automatically update what the user sees in the app based on the contents of a stream. In this case, we provide the input stream source (controller.stream
) and display different results depending on whether we successfully received data or not (in this case building terribly exciting Text
widgets). StreamBuilder
also conveniently takes care of unsubscribing itself from the stream and cleaning up after itself.
StreamBuilder(
stream: controller.stream,
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasError)
return Text('Error ${snapshot.error}');
else if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Receiving questions...');
}
return Expanded(
child: ListView(
children: snapshot.data
.map<Widget>((info) => Text(info))
.toList()));
});
One technology for both iOS and Android
"Don't Repeat Yourself" is a common software engineering mantra, yet the mobile development world seems to be in denial. All too often, companies spin up separate iOS and Android app teams, each of which needs to solve the same problems twice. With Flutter, you can write in Dart and deploy natively to both iOS and Android. Scrolling behavior, system fonts, and other fundamental interaction components automatically default for the platform you're using. At a higher level, Flutter provides Cupertino and Material Design widget libraries to get the look and feel users expect on their platform of choice.
In our Stack Overflow app, we want to have a "Get New Results" button to see if there are new questions that need our attention. We'll write a PlatformAdaptiveButton
whose behavior depends on the platform we're running on:
class PlatformAdaptiveButton extends StatelessWidget {
final Widget child;
final Widget icon;
final VoidCallback onPressed;
PlatformAdaptiveButton({Key key, this.child, this.icon, this.onPressed})
: super(key: key);
@override
Widget build(BuildContext context) {
if (Theme.of(context).platform == TargetPlatform.iOS) {
return CupertinoButton(
child: child,
onPressed: onPressed,
);
} else {
return FloatingActionButton(
icon: icon,
onPressed: onPressed,
);
}
}
}
Then, in our Flutter app, we can simply construct:
return PlatformAdaptiveButton(
child: const Text('Refresh'),
icon: const Icon(Icons.refresh),
onPressed: refreshQuestions);
Which, when pressed, requests updates from the Stack Overflow API. Flutter's roadmap calls for more built-in ways to have platform adaptive components in your code, so stay tuned.
A handful of other app development systems provides cross-platform functionality, too: React Native, Xamarin, and Ionic, to name a few. With React Native and Ionic, you develop in JavaScript, which has the potential for less type safety (and therefore more unwanted surprises at runtime), and the code is interpreted or JITed. With Xamarin, you get strong type-safety guarantees with C# and, depending on the target platform, the code is compiled to native, JITed, or run on a virtual machine. Flutter compiles down to native machine code on both iOS and Android, giving it predictable, speedy performance.
Customization
"But Emily," you say. "I work at an agency and I simply can't have all the apps I create look the same! I need them to look distinctive and add those stylish touches, like my signature, tasteful usage of Comic Sans!" Never fear, my aesthete friends. Flutter really shines in this area.
Because Flutter is drawing every pixel to the screen, everything is customizable. Don't like how that built-in CupertinoButton
is behaving? Make a subclass and design it yourself. Think solid-color app bars are so passé? Write your own widget. In our Stack Overflow app, I wrote a custom app bar that has a custom font and a gradient color scheme to distinguish it from all those other boring app bars—and it's not even much code:
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Container(
padding: EdgeInsets.only(top: statusBarHeight),
height: statusBarHeight * 4,
child: Center(
child: Text(
title,
style: const TextStyle(
color: Colors.white, fontFamily: 'Kranky', fontSize: 36.0),
),
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.deepOrange,
Colors.orangeAccent,
],
),
),
);
}
You can see the final result below.
|
|
All the code I wrote for this article can be found on GitHub at Stack Overflow Viewer.
Intrigued?
There is so much more we can do! However, further customization is left as an exercise to the reader… or as an enticement for you to come to OSCON, where we'll be coding, from the ground up, an entirely different, even more useful, and beautiful app for open source enthusiasts.
Emily Fortuna and Matt Sullivan will present Live Coding a Beautiful, Peformant Mobile App from Scratch at the 20th annual OSCON event, July 16-19 in Portland, Oregon.
5 Comments