Building a Flutter application (part 1)
Flutter is a cross-platform open-source user-interface (UI) toolkit that is based on the Dart programming language. In early July, Canonical and Google worked together to bring Flutter to the Linux desktop. On August 5, Flutter version 1.20 was released, improving Flutter's performance, expanding its widget library, and more.
In a two-part series, we will be implementing a simple RSS reader for LWN. In part one, we will introduce the BSD-licensed Flutter and cover some Dart concepts that will be used by the application. Readers may also want to consult our introduction to Dart for more information on the language. Part two will build on the basic application to further flesh out the UI features.
According to its project page, Flutter's use-case is "for building
beautiful, natively compiled applications for mobile, web, and desktop from a
single codebase.
" Its GitHub repository indicates it has
over 660 contributors who make a little less than 100 commits per week.
Google claims that the project has had
two-million developers use the framework on a variety of platforms.
Getting started
Installing Flutter is not the most straightforward task, depending on the types of devices being targeted. According to the installation instructions for Linux, the easiest way to install Flutter is to use Ubuntu's snap packaging system, but instructions for manual installation are also provided. Getting Flutter itself installed is only the beginning of the process, however. A base Flutter SDK installation enables the building of Linux and browser-based (JavaScript) applications, but the Android SDK (specifically the Android Studio IDE) is also required to target Android devices. For iOS targets, a full installation of Apple's Xcode is needed.
Most interactions with Flutter during setup are done using the flutter command, which handles other tasks like package fetching and builds. One of the more useful sub-commands of flutter during installation is flutter doctor, which checks your environment and makes various recommendations on other software that may need to be installed to enable the framework's full capabilities. It is worth noting that the documentation on the flutter doctor command indicates that the flutter command sends anonymous usage statistics and basic crash reports to Google Analytics by default; instructions on how to disable sending this data are provided.
Writing some Flutter code
To introduce the language, we will walk through a simple application that pulls headlines from LWN via our RSS feed, and displays them. Readers can find the code for this article in a GitHub repository, available under an MIT license. Here is what the application will look like running in both iOS (left) and Android (right) emulators:
The project recommends using the Android Studio IDE when writing Flutter applications (it provides a number of niceties some developers may find useful), but any editor preferred by the developer can be used. For example, creating a new application project is done by using the flutter create command:
$ flutter create lwn_flutter_demo
This command will create a lwn_flutter_demo directory housing the project with, among other things, a lib/main.dart file which, by default, is considered the entry point into the application. Within that file you would find the main() function as with other languages such as C.
Before we get too far into code, we need to add a few dependencies to our project from the pub.dev repository, which is a large repository of open-source contributed libraries for Dart (and Flutter) projects not bundled as part of the core SDK. In this case, we will use two packages: http which provides HTTP request functionality, and dart_rss, which provides RSS feed parsing. That requires updating the pubspec.yaml file for the project to add the following in the dependencies section:
dependencies:
flutter:
sdk: flutter
http: ^0.12.0+2
dart_rss: ^1.0.1
Each dependency item provides a range of versions allowed for a given package. The documentation on pubspec.yaml version constraints is helpful to understating how to express the correct version(s) needed for the application. For the package version number itself, pub.dev follows semantic version specifications. Fetching the dependencies for the project based on the pubspec.yaml file is done using the flutter pub get command.
The lib/rss_service.dart file will implement the LWNRssService class responsible for making the HTTP request to LWN's RSS feed and gathering a list of items in that feed:
import 'package:dart_rss/domain/rss1_feed.dart';
import 'package:http/http.dart' as http;
class LWNRssService {
final _targetUrl = 'https://lwn.net/headlines/newrss';
Future<Rss1Feed> getFeed() =>
http.read(_targetUrl)
.then((xmlString) => Rss1Feed.parse(xmlString));
}
There are a lot of things happening in this small block of code worth discussing. We start by importing code from our two pub.dev packages: dart_rss and http. Next, we declare the LWNRssService class. This class defines a single private variable, _targetUrl, which is scoped as private because Dart automatically gives a private scope to variables that start with an underscore.
Finally, we define the getFeed() method, which takes advantage of the asynchronous features in Dart. It returns a Future of type Rss1Feed (Rss1Feed is provided by the dart_rss package). When getFeed() is called, it returns immediately with an instance of the _Future class that eventually will become the Rss1Feed object once the asynchronous task is completed. A great demonstration of how asynchronous code impacts execution flow can be found in the Dart documentation.
The implementation for the getFeed() function is abbreviated using shorthand arrow syntax to make it more compact. It starts by calling the http.read() method that returns a Future<String>. Upon completion of the HTTP request, the Future<String> then() method is called. This method takes a function to process the String retrieved from the HTTP request (the RSS feed). In our case, we pass that String into the Rss1Feed.parse() method provided by the dart_rss package, which parses the RSS feed and returns a Rss1Feed object. This object then becomes available from the Future instance originally returned from the getFeed() call.
In a Flutter application, the fundamental purpose of the main()
method is to create and execute the foundational Flutter widget that defines
the rest of the application — where widgets are some variety of
user-interface containers built up in a hierarchy. As Flutter's core
principles state: "Everything's a widget
". Widgets are
implemented as classes; commonly Flutter applications start with a MaterialApp
class. This class implements a widget for the basic outline of an application
using Google's Material Design UI
style. Alternatively, an application could start with a similar CupertinoApp
class, which provides a foundation that is similar to the UI style of iOS.
Either can be used, or a custom one could be made; we will use
MaterialApp. Here is the code for the lib/main.dart file of
our application:
import 'package:dart_rss/domain/rss1_feed.dart';
import 'package:flutter/material.dart';
import 'package:lwn_flutter_demo/rss_service.dart';
import 'homescreen.dart';
void main() async {
final Rss1Feed feed = await LWNRssService().getFeed();
runApp(DemoApp(feed));
}
class DemoApp extends StatelessWidget
{
final Rss1Feed feed;
DemoApp(this.feed);
@override
Widget build(BuildContext context)
{
return MaterialApp(
title: 'LWN Headlines',
home: LWNHeadlineScreen(feed)
);
}
}
We begin by importing several things, including a homescreen.dart file that we will discuss shortly. We define one function, the entry-point of our application main(), and one class, DemoApp, which extends the StatelessWidget class.
The main() function is declared async because it calls getFeed(), which returns a Future. In order for the variable feed to get an actual Rss1Feed value, though, the Future returned by getFeed() must be resolved by using an await statement, which will return the Rss1Feed object when the asynchronous function completes. In a more complex example, getFeed() could have been called and allowed to run asynchronously while other tasks were addressed — only waiting if necessary at the point where moving forward required the return value of the method.
Once the await operation has been satisfied and feed has been assigned, we call the Flutter runApp() function — passing it an instance of our DemoApp class. The DemoApp class takes a single parameter (an Rss1Feed object) in its constructor.
There are a few syntax oddities here that may confuse readers unfamiliar with Dart. Unlike PHP, object instances are created in Dart without using an explicit new keyword, as demonstrated in our example with runApp(DemoApp(feed)). Secondly, the constructor for DemoApp uses a shorthand syntax. Dart constructors allow for a simplified one-line syntax when the constructor's role is only to assign any passed values directly to the object's properties: DemoApp(this.feed). For reference, the shorthand version of the constructor used in our example would be equivalent to the following more verbose version:
DemoApp(Rss1Feed x) {
this.feed = x;
}
The last portion of the DemoApp class is the build() method, responsible for returning our root widget to the SDK. This method overrides the implementation of the StatelessWidget parent class (using the @override annotation) and returns a Widget class that will serve as the foundation of our application. The MaterialApp class offers many different options passed into the class constructor as named parameters; we will only use two of them. First is the title parameter, which defines the title of the application in the user interface. The second is the home parameter, which provides the default widget to show when the application loads. Flutter implements navigation for applications using routes, and in this case, the home parameter assigns a widget to the root (/) route.
For the home widget, we provide an instance of the LWNHeadlineScreen class and pass in the feed property from DemoApp(). The LWNHeadlineScreen class provides the real UI of our demo application and is implemented in the lib/homescreen.dart file referenced in main.dart. The implementation of this class is shown below:
import 'package:dart_rss/domain/rss1_feed.dart';
import 'package:flutter/material.dart';
class LWNHeadlineScreen extends StatelessWidget
{
final Rss1Feed feed;
LWNHeadlineScreen(this.feed);
@override
Widget build(BuildContext context)
{
return Scaffold(
appBar: AppBar(
title: Text(feed.title)
),
body: ListView.builder(
itemCount: feed.items.length,
itemBuilder: (BuildContext ctxt, int index) {
final item = feed.items[index];
return ListTile(
title: Text(item.title),
contentPadding: EdgeInsets.all(16.0),
);
}
)
);
}
}
Here, the build() method returns a somewhat complicated hierarchy of widget objects defining the user interface for our application. We start with a Scaffold class that accepts two named parameters appBar and body. Each of these parameters represents another widget in the UI. The appBar parameter, which defines the application toolbar, is implemented using the provided AppBar class. In our situation the toolbar only has a title, which is set to the value extracted from the RSS feed.
The body parameter defines the body of our application, and for that, we use a ListView class by calling its builder factory method. The ListView.builder() method is given two named parameters, which specify the number items available in the list (itemCount), and a function used to construct each element (itemBuilder). The function provided to build the list items will be called multiple times, passing in the index variable indicating which item in the list is currently being processed. The index is then used to extract an item from feed, which in turn is used to build a ListTile object for each item. These ListTile objects provide a title based on the feed item. Padding around the title text was added using the contentPadding parameter of ListTile; a 16-pixel padding was generated using the EdgeInsets class.
Executing Flutter applications
Now that we have an application to test with, it can be executed on various devices using the flutter run command. Flutter attempts to guess what the target device is, but it can be specified using the -d flag (flutter devices will provide a list of available targets). Beyond running in (emulated) iOS and Android environments, Flutter applications can run in web browsers and on Linux as well. Here we show it running on Ubuntu 20.04:
One of the nicer features of Flutter is the "hot reload" feature that allows applications to be reloaded with changes during development — without a full application rebuild. Developers can start a flutter run session to execute their application, make changes to the code, and see those changes reflected in the application without having to recompile. Saving a recompilation step speeds up the development cycle considerably.
Because Flutter support on Linux is currently alpha quality, the Flutter SDK must be specifically configured to support it. This configuration can be done by issuing a few commands to use the dev channel of Flutter's SDK, and enabling Linux desktop support with --enable-linux-desktop:
$ flutter channel dev
$ flutter upgrade
$ flutter config --enable-linux-desktop
Once activated, the Flutter application can also be run as a native Linux application using linux as the target device (flutter run -d linux).
When producing a JavaScript-based version of the application, things may not always work as expected. For example, because Chrome enforces cross-origin resource sharing (CORS) policies by default, our application would not run "out of the box" in Chrome; the RSS feed at LWN does not send the necessary CORS header required to allow JavaScript on Chrome to access it. To overcome this, we would need to run Chrome with CORS checking disabled. Likewise, other JavaScript restrictions (such as local filesystem access) may also pose difficulties that require consideration when building a Flutter application intended to function on a desktop as well as in a browser.
Wrapping up
In only around 60 lines of code, we were able to build a simple RSS headline display application that worked on Android, iOS, Linux, and a web browser. In part two of this series, we will build off of the code in this article to make the interface interactive and add other features. Readers interested in learning more can find plenty of tutorials and a complete API reference provided by the project.
Posted Aug 13, 2020 13:21 UTC (Thu)
by hnoronhaf (guest, #109244)
[Link] (1 responses)
Posted Aug 28, 2020 19:51 UTC (Fri)
by bmillemathias (guest, #54343)
[Link]
Posted Aug 31, 2020 15:08 UTC (Mon)
by valir (guest, #70167)
[Link]
--
Posted Sep 9, 2020 9:31 UTC (Wed)
by nix (subscriber, #2304)
[Link] (2 responses)
... but, as I feared, horrible-looking user interface by default: would run, not walk, to avoid using any applications that looked like this, no matter what language they were written in. Does *anyone* who ever reads any text in any English-like language seriously think that a layout in which the lines of text are each separated by something like two and a half times as much whitespace is a remotely tolerable way to lay out a list (except if the list is going to a proofreader or editor who is expected to scribble things in the whitespace between the lines)? Did anyone with any experience in typography ever look at Flutter and/or Material Design at all? (And before you say "accessibility", I've got fairly serious visual deficits that impact my ability to read dense text and *even I* think this is going way too far in the direction of "nowhere near enough density". I would almost rather use a screen reader than put up with a user interface with this much spacing.)
(And presumably *all* lists in Flutter look like this unless you take special measures to stop it...)
Posted Sep 9, 2020 9:33 UTC (Wed)
by nix (subscriber, #2304)
[Link] (1 responses)
> Padding around the title text was added using the contentPadding parameter of ListTile
I misinterpreted this as meaning "padding on the left and right", but if this is padding on *all sides*, this massively whitespaced list is actually a consequence of local decisions made during the writing of this example, and does not reflect negatively on Flutter at all (except that perhaps it might be making it too easy for you to make your user interface look horrible! but that is hardly a sin unique to Flutter, god knows).
Posted Sep 9, 2020 9:40 UTC (Wed)
by nix (subscriber, #2304)
[Link]
I do wonder if perhaps I have a bit of a knee-jerk negative reaction to spacing problems in anything connected to Material Design...
Building a Flutter application (part 1)
Building a Flutter application (part 1)
Building a Flutter application (part 1)
Thanks for the great article. On small addition would be required, though. On my OpenSuSE Tumbleweed system I neede to add `--platforms=linux` to the `flutter create` command in order to get an application able to run on Linux.
Kind regards,
Building a Flutter application (part 1)
Building a Flutter application (part 1)
Building a Flutter application (part 1)