Data ostatniej aktualizacji: 08.09.2021 12:55
What is it?
What tools do we get?
Getting started – our ‘Hello World’ is all business
Taking a look around and inspecting the foundations
Flutter in a Material world
Giving our app Form
Validation – let’s put that errorColor to work
In this day and age there’s a steady influx of new, revolutionary frameworks, be it frontend-related or mobile. If one has been active in web development, she or he should be well acquainted with the constant oversupply of fresh, ingenious approaches and lightweight solutions to complex problems. This usually solves one issue and creates another – instead of wondering whether there is a technology that’s viable for us to use, we are currently left with the equally frustrating choice of which one of them we should pick.
This is why when I stumbled upon Flutter, I was quite interested in giving it a go – could it be a viable contender, or maybe even serve as a go-to solution that would give this dilemma at least a moment’s pause?
Flutter is a mobile app UI SDK by Google. It utilizes the Dart VM (which boasts to be optimized for UI specifically, also by Google), giving us the opportunity to develop for both mobile and desktop devices. Dart itself can also be used for web development, even in tandem with the all-too-familiar Angular framework, but that’s a story for another day.
It provides us with AoT (ahead-of-time) compilation to native machine code, which aims at the fastest possible execution time for the completed app, without too much of an overhead.
For developers, it offers its JIT (just-in-time) compiler and the Hot Reload feature, which enables one to change the application without losing its state – which is quite nifty, as the pain of changing UI in a ‘deep’ feature and having to navigate to it with each iteration is well known to anyone who has ever worked on UI.
An important part of the SDK is, of course, its control library. As Flutter aims at developing both for Android and iOS, it gives the option of using either Material (Google, Android) or Cupertino (Apple, iOS) control set. ‘Does it mean the application switches its looks whether it’s deployed on an Android or iOS phone so it looks native on both? Sweet!’ Not really. You can use either of the libraries, or you can use both – that much is true, but there’s no uniform UI switching functionality. It can, of course, be implemented manually – and I’m not saying that it’s something not to be done ever. Bear in mind though – functionality like that implies managing two different sets of layout controls, which can quickly turn ugly and, therefore, such an approach should be made cautiously.
Everything in Flutter is, by default, a widget. If you have any experience in Angular 2+, it’s pretty much a fancy Component and should be a pretty familiar concept. This base type contains, by default, a build method which defines the look and feel and can customize it based on passed parameters and context. Widgets can be either stateless or stateful. Stateless widgets don’t undergo any discernible mutations during their lifecycle – they are mostly static. Stateful widgets, on the other hand, are built each time they are triggered to (for example, when a watched variable changes, user performs a specific action – like a click – etc.).
This would be a good time to mention that Flutter is reactive (akin to React), which means there’s no default ongoing refresh loop like in Angular. Instead once key actions are performed – the UI or part thereof (like one of its widgets) redraws itself, according to changes in state.
I’ve already mentioned Dart boasts to be optimized for UI – what does that mean, though? In this case, rich collection handling, isolate-based concurrency and async-await with futures. I’d say this pretty much tells us that the intended application of the SDK is building business apps, rather than, say, games. It’d be wrong to assume that people won’t explore making games using Flutter though, there’s even a 2D game engine. The point I’m trying to make is that this mode of application seems like a perfect fit for this specific set of features, and that’s the angle I’ve decided to explore myself.
Although this all sounds quite good, there’s also a flip side.
Firstly, Flutter is still a fledgeling SDK. It is mature for its age, but it should be noted that it’s alpha launched in May 2017, while 1.0 was released in December 2018. Which means that at the point in time when this post is being written, it’s still just a one year old release. What are the consequences? The community – while sizable – is still not quite up to par with those of currently mainstream technologies. This affects the ability to find solutions for some common problems, and you might hit a dead end on more than one occasion – requiring from you additional work and going through specs. However, Flutter is well documented, and the community is ever-growing, so we might mark it up as a ‘work in progress’ sort of thing, rather than a distinct flaw.
Secondly (and this can be seen as both a flaw and an advantage), both Flutter and Dart come from Google. The good part of it is that Google is a tech giant, and if they want to maintain something, they have the resources and manpower to do it. The bad part is that while Google is known to introduce useful tech and services, it’s also known to kill off or retire them when they’re deemed obsolete. That’s why there’s always the risk that Flutter might eventually end up here, but that might not happen soon, maybe even not for another couple of years. So yes, it’s a risk, but then again – it’s the same for any relatively new technology, and every tech has its beginnings.
Flutter can be developed from within the most common programming IDEs – we’ve got Android Studio, IntelliJ IDEA, there’s also a Visual Studio Code plugin – which means that most developers won’t have to stray too far from their default environment. In my case, as recently I’ve been doing more web-oriented work, the choice was VS Code, but this shouldn’t affect the development in any meaningful way as text files are still fortunately just text files. The target platform will be Android (the reasons for this choice are quite down-to-earth – I simply neither own an iPhone, MacBook, nor even an iMac), so it looks like I’ll be installing Android Studio anyway – for its VM.
Aside from the IDE there are also the Flutter/Dart DevTools, which are a suite designed to monitor the app’s performance and provide some debugging instruments, like the Flutter inspector, which acts pretty similar to its WebTools counterpart. The real-time resource monitor is potentially a huge help in finding the app’s performance bottlenecks and the hierarchical inspector – in seeking out possibly redundant nestings, which plague UIs of many apps and websites alike.
What’s more exciting than writing a mobile application for managing your insurance policies? It’s important to note that I might try to do some things in different ways, resulting in code inconsistencies. The app is supposed to contain some ideas and examples on how to solve common scenarios, without deciding on which one is best.
A simple overview of the app-to-be:
Our API will be simulated by Mockoon, I’ll be using VS Code as the IDE, and the device will be provided by Android Emulator (I’ve settled for a Nexus 6 API 28). As a starting point I’m using the official guide and empty app created according to Flutter’s official website, followed up to the point where we have a barebones Flutter project. In my case it got me the structure on fig.1. You’ll find the complete app code here, and I suggest browsing it alongside this post, as it will be referred to all the time. This part of the post is, after all, mainly about pointing out potentially useful chunks of code and the purpose they could serve.
fig.1. You’ll find the complete app code here, and I suggest browsing it alongside this post, as it will be referred to all the time. This part of the post is, after all, mainly about pointing out potentially useful chunks of code and the purpose they could serve.
Fig. 1: The initial state of the project
The file pubspec.yaml holds the project’s dependencies, assets and version number – pretty straightforward. It’s also seeded with lots of informative comments, but we won’t be doing much work with it, at least not on a daily basis. What’s most important for us is the lib folder, as there lies the root of our application, the main.dart file, and in it – the main() method. This is the entry point to our application, and no code should go beyond that point. Alright, time for some scaffolding.
The homepage is the default view, or route, of our application. There’ we’ll be displaying a bunch of policies. So, surely getting the dictionaries from our api would seem appropriate. I’ve built a singleton service that calls the API service and makes the dictionaries available before the application even starts, so that the data is readily available wherever in the application we end up. It’s called CommonData, and the dictionaries API service – DictionariesService. Both are located in the lib/services folder. I’ve also added a helper service (called Helper, another naming masterpiece) for universally used functionality, like a default padding, common conversions etc.
Fig. 2: commonData.dart
CommonData (fig.2) is a singleton with an internal constructor, which stores its only instance within a static field of itself. In the app we won’t be using the CommonData class definition anywhere else – only its commonData instance declared in this file. The DictionariesService.get() method returns a Future<DictionariesService>, which is basically a promise. This means we can either await its result and continue with code execution of initialize() once everything’s ready, or use a .then(…) and return early. We want initialize() to finish once we’ve received a response, so we’ll use await. We’ll get to the implementation of DictionariesService.get() later.
After a bit of research it turned out hooking commonData.initialize() to run before the UI even gets drawn is quite trivial – it’s enough to place it in our main() (fig. 3).
Fig. 3: commonData gets initialized before we even run the app itself
This way wherever we are in the app, we’ll always be sure commonData was initialized, as the app itself is executed AFTER initialize() completes. Such a solution could be useful in many cases, like a server-stored application profile or theme, data staging, application setup etc. In case of asynchronous operations, thought, we should be handling them on the home screen, where we can display some sort of loading indicator (which we’ll see in action as well). This would prevent the user from seeing a blank screen on startup and wondering whether the application crashed. That’s why if we absolutely have to do something before the app properly starts up, it’s probably best to stick to operations with a predictable, negligible execution time or create a separate ‘loading’ screen with some animation and a clear ‘loading’ message to put the user at ease, do it there and navigate home upon completion. I’m leaving this ‘awkward preload’ in the application as a sort of a UX anti-pattern.
Let’s take a look at the MyApp class, located just below main(). Its body is mainly the overridden build(BuildContext) method – which is called every time the MyApp widget is being redrawn. Our app has more than one screen – home and 5 steps of the policy registration wizard (policy type, product, covers, owner, and subject), hence I’ve conducted a careful study of the subject in question (fig. 4).
Fig. 4: Flutter application navigation research
So, navigation in Flutter is called ‘routing’. I’ve created some routes according to one of the many tutorials (fig. 5). A default, initial route – this is our MyHomePage widget – and five wizard steps. We’ll see if we will need to access the build context, but it’s nice to have it on standby.
Fig. 5: basic routing in a Flutter app
This is a good moment to mention that, because our app uses the Material control set and is a MaterialApp instance, we can quickly change its aesthetics following the Material Design principles. The ThemeData class contains ‘color and typography data for a material design theme’. It can be accessed within the application via a static method: Theme.of(BuildContext) and hooked up to various properties if we need to change their default, theme-driven value. For now we’ll just set the primarySwatch (the leading color of the application and its various shades) and the accentColor (also an assembly of color shades, the app’s de facto secondary color). If we stick to using the theme’s defaults and/or generated values (which we will try to do), we should end up with a more or less visually appealing UI. If we don’t want to use the default color swatches, we can easily define our own (fig.6). It’s a lot of conceptual work though (unless we’re given a style guide by the client), and I would like to avoid creating some sort of aesthetic abomination, so I’ll keep it simple. There is also a myriad of material color swatch generators on the web that offer the option of generating one if you provide the ‘primary’ shade. There is an option of setting the errorColor, but as a person that has had his toe stuck in the UI/UX field, I advise you to approach it with caution – the standard red is pretty much the error indication industry standard. Avoid changing it if the color scheme allows us to do so, maybe change the shade just a bit?
Fig. 6: an example custom color swatch.
The homepage is basically a list of tiles which represent individual policies and expand to show their details, there is also an option of registering a new policy. The tile should therefore be stateful, as its look mutates, but the page can remain stateless. Yes, it displays a list of a variable length, but its elements – nor their values – do not change during its lifecycle. Note that if we didn’t separate the tiles into standalone widgets (and instead handled everything in one monster of a class) then it would have to be stateful.
Fig. 7: MyHomePage data initialization
Let’s start with the data needed for our route (fig. 7). Whatever logic we place here will be executed each time we navigate to ‘/’. In this case it’s convenient – each time we end up on the home screen, we’ll have up-to-date account data and a list of registered policies. That way we’ve already solved a problem we’d be facing in the future: how to refresh the home screen after completing the wizard; Now all we have to do is navigate back.
Inside MyHomePage (homepage.dart) you can finally see some UI definition. The root of our page is an aptly named Scaffold, which lets us set an app bar, an action button, the body of the document and various other options – effectively a template for a general purpose mobile app. If undefined, the part will be omitted (i.e. no footer = no footer, not an empty footer). The appBar is minimal, there’s a floatingActionButton to initiate the new policy wizard, backgroundColor has been hooked up to the current theme’s backgroundColor (to maintain consistency if we decide to change colors), and there’s of course the meat of the matter – the body.
The policies, as noted earlier, are wrapped in a Future – they aren’t ready to be passed along to a simple ListView. That’s what FutureBuilder<> is for: It’s in fact a widget, that returns content based on a Future’s internal state. Using the snapshot (AsyncSnapshot) variable we can return different widgets depending on whether the Future has already finished or is still in progress, or if it contains an error and so on. In our case we’ll return a ListView if it’s done, and a loading indicator if it isn’t – pretty standard stuff. It could probably be a good idea to wrap any possible error handling for this into some sort of a universal method in the Helper class that accepts the snapshot.connectionState and outputs some generic error, there are many options on how to solve borked Futures – here, for the sake of brevity, I’m using none of them. It’s done or it’s loading.
Fig. 8: FutureBuilder in action
Moving on to the HomepageTile widget – our first stateful UI part. Every StatefulWidget consists of the widget declaration (fig. 9) and its state – and the state is where the magic happens.
Fig. 9: The StatefulWidget. Not much to look at here – practically a state factory
The UI of the widget is defined in the state, in its build method. There, every use of setState(fn) tells the framework to rebuild, reevaluating its build(BuildContext) method with updated property values. Here I’m using the _expanded field value as a condition whether I return the mini _buildMiniTile() or verbose _buildMaxiTile() widget version. It could, of course, be a matter of just a simple conditional assignment, but let’s make it look better with an AnimatedCrossFade widget. It does exactly what it says on the tin – it crossfades one child with another according to its crossFadeState (fig. 10). Thanks to the fact that on each setState the widget gets rebuilt it’s possible to juggle between more than two states, but it’s a rather unusual scenario – getting to a state with a specific number of taps sounds a bit like teasing the user or playing a ‘hidden object’ game unless very strongly visually implied.
Fig. 10: AnimatedCrossFade, driven by the _expanded value
Alright, so we know how to create a home screen with generic tiles mapped to user’s policies. The time has come to see how the app is being fed the data we’ve got set up in our Mockoon API. For this purpose we should open up the DictionariesService (fig. 11).
Fig. 11: DictionariesService – not much, but enough
As you can see, the get() method is marked as async – this means that whatever we return will be wrapped in a Future<>, to accommodate its promise-like handling. The http client executes our command asynchronously and provides the response, status code and all. Just below we’re mapping json (whose type is by default Map<String, dynamic>) to our DTO objects. Since these are dictionaries, I’ve taken the liberty of creating maps for them so we won’t have to iterate through all the entries when we need to display a name corresponding to a specific code (i.e.: commonData.maps[DictCode.PRODUCT_TYPE][_policy.type]).
Next, let’s take a look at our DTOs. There’s no ‘default’ option of turning json into objects, but fortunately there are plugins. In my case it’s json_annotation which, once started as a watcher with ‘flutter packages pub run build_runner watch’ will look for the @JsonSerializable annotation and create mapping functions – as we can see on fig. 12-13.
Fig. 12: policy.dart – our DTO class
Fig. 13: policy.g.dart – generated by json_annotation
Two important parts of almost every conceivable business app out there are forms and validation. Let’s see what we’re working with while checking out code for the insurance policy wizard. The first two steps (1_newPolicyType, 2_newPolicyProduct) are all pretty standard stuff found in all the other ones so i’ll be skipping them. If you want to see an example how to asynchronously perform a calculation while the use is filling the data in, check out the 3_newPolicyCovers step – it contains a dummy implementation for one of life’s greatest mysteries, premium calculation.
Definition of forms looks pretty standard – we define a Form object, handle it a pre-generated GlobalKey<FormState> key and then define its elements, as seen both in the 4_newPolicyYou.dart file and on fig. 14.
Fig. 14: 4_newPolicyYou.dart – pretty straightforward form declaration. Note the use of Helper to minimize code clutter.
The form can interact with the data in many ways, so it’s possible to design it in line with the developer’s preference. If we want a pseudo two-way-binding behavior, we can persist the value in the onChanged handler inside a setState wrapper. We can, however, just use the dedicated onSaved and persist the data once the form is all ready – which is the course I’ve decided to take. The Step4Builder class (fig. 15) holds the ever-present wizard sequence – if form is valid, save and move on. Injecting data into the form is handled with ease – since we’re passing values from the model (processData) into respective controls’ initialValue, they will update with each setState operation. That’s why we can simply fill the model’s fields (processData.setOwnerFromAccount) and then reset the form using its key (this._formKey.currentState.reset) which will cause it to reevaluate the initial values of the fields – grabbing them straight from the model. Why reset the form at all though? This will ensure that the fields we didn’t fill in setOwnerFromAccount get assigned their default values, which will still be in the model, as long as we don’t persist the form-stored values.
This is just one strategy – in different scenarios we might encounter different preferred solutions, but it’s easy to notice that we aren’t forced to deal with them in a specific way.
Fig. 15: Step4Builder – if valid, save and go to the next step. Nice and clean.
Achieving dynamic form layout does not differ much from classic js/html shenanigans. On the wizard’s final step, 5_newPolicySubject.dart, we’re supposed to register data of the policy’s subject, which implies the use of different forms depending on its type (a car, a person, a lizard, and so on). We’ll achieve that by defining different fieldsets defined in separate widgets and showing the ones that fit our choices in the previous steps. In the application there’s only one type implemented (reptileObject.dart), but more can be added simply by performing a check in the build method (fig. 16).
Fig. 16: 5_newPolicySubject.dart: since all I’m thinking of is insuring my pet which (presumably) is a lizard, the only available form definition is the ReptileObject, but nothing prevents us from inserting an IF statement into the child property that will supply the correct one.
Okay, so we’ve got some textboxes and a dropdown – time for everyone’s favorite control, the date control… which does not exist. This may seem strange if we’ve never had a chance to develop a mobile app but, when we think about it a little longer, it makes perfect sense. It’s always preferable to use the native system’s method of input (for example, we do not define our very own special keyboard-control-3000-XP, we just use the one provided by the system), and each mobile system has its own date input method in the form of a calendar. That’s why our ‘date input’ will be just a read-only TextFormField, which will ask the system to supply its value when we touch it. An example is contained within the aforementioned reptileObject.dart file (fig. 17-18).
Fig. 17-18: reptileObject.dart – The TextFormField has a very limited amount of responsibility – to tell the system that the user needs to input a date, and to display the result of this action. We’ve defined an onTap handler to intercept any attempt to interact with the control and make it show the system datepicker instead. As it’s an asynchronous action (the user can take as much time as required), the whole method is appropriately marked.
Now that the form is in place, all that’s left is to provide some rudimentary data validation. The convention is quite simple: each form control has a ‘validator’ property, which accepts a function, with the value as the input and a string as the output. If the output is non-empty, its contents (which are considered a validation error message) are shown in the appropriate area. A simple example of combined validation (two criteria, two messages) can be seen on fig. 19.
Fig. 19: A simple validation – if Validations.required returned an error message, return it. Otherwise check if input is a valid email address. If not, return our custom message.
This is all good and well, but what in case we have to perform an asynchronous validation, like a username availability check? Well… tough luck. Flutter does not support Future<> in validators, and most likely it never will, as it was stated that it could break sync validation, and mixing the two isn’t a good UI practice anyway because reasons.
Even if we take this as a fact, it does not prevent us from facing a scenario in which we simply must perform a validation server-side, with the only alternative being loading gigabytes worth of data into the device’s memory. Fortunately, there’s a sort-of-accepted workaround which is pretty simple. In the validator, we perform the call and toggle a local flag. If the flag is up, we don’t display any validation message. Upon the call’s completion, we save the result into some local variable, toggle the flag and manually trigger the form’s validation. This way the first time the validator is triggered no message is shown (or we might show a ‘please wait…’ to indicate an action in progress), and the second time it changes to the action’s result (which would have to override the hypothetical ‘please wait’). The whole process – with an example – is available in the linked post.
So yes, while async validation is possible this way, it stands to reason to expect the SDK to support it out of the box. It’s possible, but it could be cleaner.
Nevertheless, we prevailed – our app is up and running, and it didn’t even take much time, all things considered. We’ve covered most of the basics when implementing a business app, and there weren’t really any roadblocks – all in all, I think we could mark it off as a success. We’re ready to remove the loading anti-pattern, clean up, integrate with a backend, and do a complete rework after the first round of customer feedback.
Home screen, default and expanded
Covers selection, better get that ‘bad puns’ one
Policy holder data, with an autofill option
Policy subject screen, count these legs carefully
So, should you use Flutter for creating a mobile app? To decide that, I think one should consider a few things in making this decision – and for different people the final answer may vary.
If the mobile app you’re about to write is your first of its kind – I’d say go for it. Flutter has got a quite accommodating learning curve, and does not require any obscure knowledge. The tutorials and materials available make it pretty easy to determine what can we use in specific scenarios, and what tools we have at our disposal. When learning a framework that’s been around for years, some practices can be deemed too obvious to describe, consequently making them very hard to find out about. As it’s a relatively new tech, no question is too obvious, and there aren’t many oh-everybody-knows-that tricks buried forever under a mountain of new feature issues. For an experienced mobile developer, on the other hand, things stand as with every other new technology. When committing to write an advanced, multi-feature app, the bigger it is, the better it is if you’ve got any experience in the technologies used. However, if you’ve got a small app to write, Flutter might prove an invaluable tool in rapid development.
In terms of the community, it’s still growing. It’s not overwhelmingly vast, but it’s not miniscule either. Opinions on this may differ, but I think its current size warrants small- to mid-sized app development. The bigger the user base, the more edge cases have been researched, and the bigger the chance you’ll find help when in need, so since the community is steadily growing, large-scale projects are becoming more and more viable, and less and less of a risk – provided it won’t be killed off.
There are currently many apps developed with flutter – it’s not an exotic, niche framework anymore. As seen on the official website, not only Google uses it, but some big-brand companies as well. This bodes well for the technology’s support plan, and is quite an enticement to at least giving it a try. Considering how relatively fresh the tech is, that these companies had probably had to do a little RnD before greenlighting a public app, and they still went with it – It does not seem like using it is that much of a risk anymore. It’s certainly viable and, given time (if at some point it won’t get bogged down with a hefty overhead and overly-complicated architecture), it has a chance of becoming a go-to solution for mobile apps.
As we all know, the market can be fickle, trends change and all that… But that should never stop us from exploring the new. And, in the end, Flutter seems worthy of our time.
Author: Wojciech Kuroczycki, Lead Developer