Breaking the fall on a hard piece of Flutter: a fail-safe way of getting ready for the first project

Breaking the fall on a hard piece of Flutter: a fail-safe way of getting ready for the first project

If you’re only thinking about starting to write your first Flutter project, you may be unaware of all the pitfalls hidden under the foliage of the seemingly short learning curve in this technology. 

Chances are you have more questions than answers. Should you consider localization at the start and what approach should you choose? Is it okay that I’m an Android developer and know absolutely nothing? If a tester asks me to put together several build configurations for them, do I update my CV straight away, or is it not that scary? On top of that, it’s this Dart... it looks like a typical language, but every so often you have these flashbacks from your dynamically typed past.

This article will be your torch in the dark, uncharted cave of horrors called “My first project on Flutter”.

Hi! My name is Eugene Saturov and I am the head of Flutter development at Surf. We’ve been using Flutter for around three years, and we’ve taken so many knocks it’s scary. And I'm even more terrified when I think about all the people out there in the same place we were three years ago: about to use Flutter in their next big, medium, or small project. 

I’ve tried to put together the most valuable pieces of advice in this article. The following is a transcript of my lecture. If you prefer watching to reading, check out the video.

First steps

Learn the basics of Android and iOS.

Flutter supports six platforms, but it’s still mainly a framework for Android and iOS mobile development. 

It’s a common pitfall for novice developers: when they start learning Flutter, they begin with the obvious—Flutter. I recommend that you don’t start with Flutter but rather cover the basics of Android and iOS. Sounds counterintuitive, but you’ll have to learn them anyway. Only it’ll be stressful and painful when you do it in the middle of developing an actual product.

I recommend learning at least this much about native platforms:

  • Understand the structure of Android and iOS projects: the constituents and layers.
  • Be competent in the syntax of Kotlin and Swift. You’ll deal with Objective-C much less often and see even less of Java.
  • Understand the way the key platform features work: e.g., permission — you need to request it really often. There are some off-the-shelf plugins that take a lot of the load off your shoulders. Still, you need to know the specifics and differences between the platforms: they affect the way your app behaves.
  • Understand the way an app works in background mode. When an app is in the background, it uses the battery and computational power of your device. It all contributes to the negative user experience with the device.
  • With each update of an operating system, vendors limit the background capabilities of apps. You can't bar apps from working in the background. That’s why OS developers and vendors came up with other restrictions. You need to understand the way it affects us: some things a business might request may not be feasible at all. This affects the approach to project development. Each platform has its own nuances.
  • Never forget about push notifications. Typically, everyone uses Firebase to deliver push notifications. It takes care of the toughest part of the task. Nevertheless, I’ve seen cases—more than once—where push notifications are successfully delivered to Android but not iOS. The most trivial reason being—developers forgot to request permission for push notifications in the app. It’s a must on iOS and absolutely unnecessary on Android: you don’t even have such a permission there. 

Get to know all the out-of-the-box widgets

There’s no guarantee that you’ll need this advice. Perhaps you can easily find your way around the family of widgets. But if you can’t, I highly recommend purposefully looking at the way they are organized and using them in samples.

I doubt that someone actually sat down, opened the widget catalog and analyzed them one by one: examined their capabilities, tried them in action, looked into the API. Nevertheless, knowing your widgets is helpful. We sometimes try and solve a task with a widget cronenberg when later on it turns out that there’s an out-of-the-box widget that goes about the task in a much simpler manner.

Widget classification:

  • Simple and commonly used widgets: e.g., Expanded, Flex, Wrap. They’re simple and may resemble each other in a way. As a result, people occasionally mix them up and then have to clarify them and read the documentation.
  • Simple field-specific widgets. A vivid example is a widget called Divider. If you use a regular container that is 1–2 pixels high and painted gray or any other color, you get the same Divider. It doesn’t seem like a dramatic difference: Divider or container.

In fact, however, Divider makes layout more declarative: it separates one block from another, not only visually in the interface but also in code, dividing one block from another. So, using Divider is actually important.

  • Complex positioning widgets, such as CustomMultiChildLayout. We get used to constructing UI with simple widgets, so much so that we’re not too eager to get familiar with the complex ones. And that’s wrong since, at times, it results in suboptimal code. 
  • Exotic positioning widgets: IntrinsicHeight, OverflowBox, FittedBox. If you don’t know they exist, you obviously won’t consider using them.  
  • Sliver widgets. Sliver widgets mostly play the role of adapters for other widgets that can be used in the Sliver environment. First off, you need to understand the way the whole Sliver thing works. It’ll make your life so much easier when you need to tackle a tricky task and implement a cool design.

Understand the way constraints work

Constraints are crucial when you need to build the layout of a user interface. According to Flutter developers, it’s the least intuitive thing in Flutter: it differs a lot from the principles we are used to sticking to in imperative UI-frameworks. 

Here’s a situation: we have a container that’s given a fixed height, width, and color. Out of all the properties, the only one that applies is color. Height and width — don’t because the container is stretched full-screen or fills the entire space available inside the parent widget.

No alt text provided for this image
Pitfall No.1

How come? It’s not a bug—it’s normal behavior. There’s a long page dedicated to it in the Flutter docs called Understanding constraints. It gives over 30 examples of various widget combinations, each revealing a specific nuance of working with constraints in layouts. You have to understand these: it’ll make building layouts so much easier.

Project onset

Not all things can be kept on the backburner: some have to be dealt with straight away. Otherwise, you either won’t be able to do them at all or it’ll take inadequate amounts of time, money, effort, and energy and won’t do any good to the project in general. I’ve tried to put together all the tips that could help you do all the important stuff right at the start.

Configure the release build.

Don’t put it off until the release is around the corner: you’ll need to fix loads of bugs, and the release build configurations will only add insult to injury.

Configuring an Android build is pretty easy: you may have a false sense that it’ll be just as easy on iOS. And that’s when the fun starts: figuring out the way it’s done on iOS takes way too long. Provisioning profiles, certificates. Where do I specify devices and do I need to? How do I link it all to the console? How do I deploy the artifacts anyway? What’s the right way to gather them?

Sure enough, you won’t do without Xcode. You’ll have to figure out its remarkable interface and the way to configure it all too. That’s why the first thing you need to do right at the project onset is configure it all in advance.

There’s a great article on why Apple implemented app signing the way they did: when I read it, it made things much clearer. I recommend checking it out and searching for enlightenment there.

The article — Debugging how you think about code signing >>

Configure CI/CD

As for all things CI/CD, there are two camps that go head to head. The first one is the people working for large companies. They must have DevOps departments that configure CI/CD. All developers have to do is say, “I want CI/CD” and in a couple of days they have CI/CD with all the necessary configurations and steps needed for development.

The other camp is the people working for a small startup: two developers, no DevOps. Basically, what you do is what you see. Naturally, no one gives you extra time to configure CI/CD.

The good news is: you can deploy a handy-dandy CI/CD virtually at bench scale. All you’re going to need is Github Actions. It’s a handy thing that really shortens the learning curve.

You can deploy it and write a plain pipeline, even if you don’t know the first thing about configuring CI/CD, and implement all the necessary steps. I especially recommend focusing on the build: it’ll give you an understanding of whether your code works or not. If your build has crashed, there’s probably something broken in the code.

The project could also use some steps with test runs and styling. But that’s optional: e.g., if your project has no tests, the test run step is absolutely useless. If you believe that perfectionism in styling is too much, you can avoid that step as well. But I heartily recommend keeping it anyway.

Nota bene: even if your project is in a private repository and your Github Actions is fee-based, you can still cut the expenses. All you have to do is deploy your own self-hosted runner and connect it to Github Actions using dedicated software: you’ll be able to build the builds as much as you want as long as the machine is online.

Provide for localization.

Even if your app is monolingual and nothing tells you it’s going to have more languages, I’d recommend laying the groundwork for localization anyway. It won’t take much time at the start. By contrast, writing an entire app over again when localization wasn’t provided for is practically impossible.

If you lay the groundwork for localization at the start of your project, you’ll have one more thing stopping you from working with strings on a level of architecture where it should never be used. You have to be strict about where to use strings: it’s either the UI or the presentation layer. No matter what architecture you choose, the layer of business logic clearly isn’t the place to use string resources. 

My option of choice is intl. If you feel intimidated by the enormous size of intl and the abundance of boilerplate code you need to write, you’ll find the Android Studio and VS Code plugins pretty helpful. They’ll generate the atrocity themselves, and you’ll find working with localization a bit more enjoyable.

Configure several types of builds.

Providing for several build configurations is a powerful tool, though rarely used. Yet it really speeds up the process: you can spare yourself needless rebuilds by isolating mutable configurations to another level of implementation. Some configurations can be changed “on the fly” without having to rebuild an app. To tackle this, we have a wide range of tools. 

Flavors and schemes in Android and iOS. All these have got to do directly with the native platform we’re building our app on. Bundle ID, name of the app, icon—all of that is configured through flavors and schemes, because there’s no other way you can affect these build configurations. We configure several types of builds with different settings in advance.

Entry points are the great thing about Dart apps. You can change it when you build an app: create several main-files and build your app using different main-files. Thanks to that, you get ample opportunities to configure implementation details: you can use main-files to keep the global configuration which defines, say, the URL of a server that your build refers to. 

Testers like to have a choice of several builds that refer to different servers: that way, they don’t have to manually create a new build with a specific server every time they need to. Such builds configured using different main-files are a real lifesaver. Besides, if your project has a feature toggle, you can configure it here as well.

dart-define is a command you can use to pass environment variables. Here at Surf, we don’t actually use it—that’s the way it’s been. I know there are adepts out there who love --dart-define, but we deal with things by changing main-files and are perfectly comfortable with that.

Theme and styles

Styling settings affect even more isolated parts of an app than localization or string resources. That’s why you either do themes and styling the right way or not use themes at all and configure the way widgets look “on the spot”. 

Picture this: practically every widget has configurations—colors, rounded angles, shadows, text styles, etc. Should you happen to need a global redesign, the simple task of changing one property in a single spot in the app theme turns into a task that would take weeks: You’ll have to wander around the entire app looking for properties.

That being said, there are a couple flies in the ointment. First off, you need to be careful when working with themes. Some theme properties may be responsible for quite unexpected things: for example, canvasColor. Back in the day, we came across a bug: the context menu that was called when we selected text in the text field was illegible because it was black text displayed over a dark-gray background. 

No alt text provided for this image

We were racking our brains over that for a long time. Eventually, I popped into the source code of the context menu and noticed that the background was passed there through a series of levels from a canvasColor property. The dark canvasColor was specified in the theme applied globally to the entire app: and we had implemented the dark mode. So, the solution turned out to be wrong.

You can find such things in abundance in the Flutter framework. I really don’t recommend changing theme properties if you’re not sure that the property is only responsible for the thing you need. I recommend styling either inside the theme or the body of a reusable component—but it would be best to refer to theme properties if you do that.

But honestly, it all depends entirely on you here. If the designers don’t stay within the design system and don’t create a UI kit, they may “cripple” the consistency of layout components they themselves created. Then, all the nice themes they’d configured would go up in smoke: you’ll have to hardcode styling settings right on the spot. That’s why I recommend doing all you can to make such a UI kit happen in your project. 

But if it didn’t, I recommend checking out the open source project called Widgetbook.io. What it does is give you a full, nice, and graphic picture of all the screen widgets in your app. I think that for a big project that has plenty of reused components, it’s going to be a pretty good aid when you onboard new team members.

No alt text provided for this image

ScreenUtil is something that I recommend using if you’re targeting devices of different form factors and have no intention of spending too much time on adaptability.

With ScreenUtil, you can get adaptability practically “free of charge”. Simply set variable height and width values using the respective extension. What it does is scale the specified values and make them relative to the size of your device. ScreenUtil has proved to be a quick solution, giving pretty good results.

No alt text provided for this image

Productivity

The Debug screen has landed a big helping hand in communicating with the QA department. It has the features primarily used by testers. For example, it allows changing the app URL on-the-fly: all you have to do is open the debug screen, choose another URL, restart the app, and here it is referring to another server. You don't even need to use another build, which saves testers a load of time.

What’s more, you can use this screen to configure a proxy. You need that to:

  • Connect to tools that modify server responses and mock server behavior—for example, Charles. Testers would be grateful for it. 
  • Use the screen to demonstrate the UI kit or demonstrate notifications, and to turn the feature toggle on and off. Sky's the only limit here for you and the testers.

Side note: remember to get this screen out of your production build. Better yet, write it in a way that makes the entry point out of reach from the production build and do it right away: there’s no use in it for the real life users.

In our case, the debug screen looks like this.

No alt text provided for this image

It may differ a bit from app to app, depending on the project specifics. But you can also implement it your way. It’s just that the idea seems pretty interesting and somewhat underrated to me.

Navigation

In Flutter, navigation is pretty well-implemented out of the box: you can take the simplest imperative navigator and write a big project with it.

That’s what we'd been doing for a long time. But over the three years of working with Flutter, we’ve learned the following: if you code a project long enough, there will come a time when you’re tasked with deep links. And that’s when you’ll realize that there actually are other ways of implementing navigation where you could have added support for deep links in virtually two lines of code.

If that’s the thought you’ve had as well, you might want to check out either Navigator 2, although it gets all kinds of reviews, or such solutions as go_router, which is now actively promoted by Chris Sells from Google (UPD: unfortunately, Chris has left the company a little while ago)

There are several popular plugins I can recommend — for VS Code in particular. 

Better Comments—highlights important comments in code: there are preset styles for standard comments, but you can make custom ones.

Color Highlight—goes through the source code, finds the lines containing coded colors, and highlights them with the colors specified.

Rainbow Brackets—highlights bracket pairs: each pair with their own color. The open brackets are highlighted in red.

Pubspec assist—with it, you can add dependencies in pubspec.yaml right in the editor in Dart or Flutter projects.

If the plugin you need doesn’t exist, write it. And if you don’t yet have an idea for your own plugins, I can share the most basic idea of all, which came to everyone’s mind. I’m talking about plugins that generate template code that you commonly use.

If you’ve chosen a specific architecture for your project, you’re likely to create an entire hierarchy of classes for a screen every time you create a new one. More often than not, it looks like copypasting first. And only then do you start adjusting it. Generating these screen carcasses can easily be automated with your own plugins, which is exactly what our team does at the moment.

Fun fact: with Flutter for Web released, you can now debug not only on a real device or emulator when you write for Android and iOS, but on the web too. You can build an app, run it in your browser, fit it to the screen of your device, and use it to debug the app.

Of course, there are a lot of ifs and buts: your project shouldn’t have libraries that are not supported on the web. In fact, you shouldn’t have any features that break the way the app behaves on the web. So, you probably won’t be able to use it in complex projects. But if you have something super simple, you can definitely give it a try.

UPD: An even better substitute for an emulator is Flutter for Desktop. Build your app as if it’s supposed to work on a desktop and debug it without any lags or problems. Keep in mind, however, that platform integrations with mobile platforms can not be tested like this. 

Generating a network layer. Swagger and OpenAPI are open source protocols that you can have considerable freedom in implementing. You can see it if you try OpenAPI validators in action. There are plenty of them on the marketplace, for example, VS Code. You can install 10 validators and each of them will find the errors the others haven’t.

It’ll all affect you once you try to generate something: generators typically need documentation to be kept in a specific format. It’s pretty hard to move away from that. 

Here’s when I’d like to promote our solution, called SurfGen. We’re actively improving it at the moment: it may become the main generator that helps in 100% of a hundred cases. Meanwhile, however, I can’t say it’s ready: it’s currently undergoing in-house test runs. We’re really ambitious about it.

Beware of Dart!

Mixins. Dart is a wonderful language that has become better and gained a ton of unique features and capabilities over the last few years. On the other hand, like any other language, it has some things you should really treat with caution, such as mixins. When you run into them for the first time, you’re like, “Wow! I can just about write multiple inheritance now!"

Although, of course, you have to understand that mixins aren’t multiple inheritance. It’s just a small feature that helps increase the share of reusable code in a project. You need to be careful with it: if you know what will be typed in the console after the code is executed, great job. At the very least, you have a pretty good idea of how the mixins work.

No alt text provided for this image

However, the mere fact that IDEs, analyzers, and compilers won’t stop you from writing this code is pretty dangerous: you can get a weird heisenbug in behavior when you duplicate the names of your variables and methods here and there and apply them to the same class. Back in the day, I was confronted with this very trait they have and have been suspicious about mixins ever since. 

Exclamation point and null safety. With null safety around, Dart has become a different language altogether. But at the same time, there’s now a way to shoot yourself in the leg, and a pretty tempting one at that: I’m talking about the exclamation point you can now use to refer to a nullable variable, ignoring the safety provided by null safety. 

This specific thing isn’t that obvious in Dart: if you refer to a global variable that has a nullable type, even the null check in the previous line doesn’t guarantee that the variable will be non null from then on.

No alt text provided for this image

You just won’t be able to refer to the tail in tail.cut. The compiler won’t let you: instead of tail and a variable, we may have a getter which may suddenly return null even if you make two subsequent calls.

And that’s when you’re always tempted. You’re all like, “Look, I’ve checked it. It can’t be null. It’s not a getter". You put an exclamation point, refer to the variable, and it’s all good — right until someone comes along and changes the variable into a getter. But when would that be? Could that even be?

No alt text provided for this image

This starts to spread into other use cases of the exclamation point, and before you know it, you’re putting it everywhere. And sooner or later, something crashes in runtime, or worse, in production. That’s why it’s better not to use exclamation points.

Typing

Switch off implicit-casts and implicit-dynamic: it’ll save you a lot of energy when debugging. And don’t even get me started on dynamic: there are almost no cases left where it makes any sense to use dynamic by now.

Configure the static analyzer in the strictest way possible. The stricter the configurations, the fewer random mistakes are left unnoticed in the code base. There’s practically no reason not to do it. Instead of sweating over the configurations yourself, you can try our solutions. The package is called surf_lint_rules and it’s available on pub.dev. It has extremely strict configurations and will provide you with the ones that we use in our daily work.

Don’t turn a blind eye to warnings: even if you have the strictest configurations, but they are all warnings that don’t require fixing and you don’t pay attention to them, there’s absolutely no point in such configurations. Develop a zero tolerance for warnings in your team. And try to curb warnings as much as you can.

RxDart 

You’ll only need RxDart for complex operations with data flows and data flow synchronization. We had been dragging RxDart into our projects for a while out of habit, because many of us immigrated from Android development. On Android, RxJava is a pretty popular thing. A while ago, Android offered no adequate way of interaction with asynchronous operations: a lot of abundant and messy boilerplate code. Writing a good implementation was pretty hard. RxJava took handling asynchronous processes to the next level.

As it turned out, using RxDart makes 25% of sense compared to the initial thing. Dart has all the necessary classes to handle asynchronous stuff out-of-the-box: for example, it can receive delayed operation results with Future; it has streams to work with data flows. If you need some grown-up background operations, use Isolate. 

What we have left in RxDart is probably just a smidgen of complex operations with data flows, things that you by no means will encounter in every project: synchronization, complex filters, mapping, and so on. 

Code quality

A few tips on organizing your project. 

  • Group project files according to features. It’s the easiest way to get around a project if it’s not microscopic in size.
  • Naming. File names should always correspond to class names. Otherwise, you’ll be spending a long, long time looking for some classes in your code base. Especially if you forget their precise name.
  • One file only contains one class. It’s not that obvious and makes for a helpful rule that makes it so much easier to navigate a project, especially if you’re new to it.

If we’re dealing with widgets, I recommend disregarding this rule because a widget is both a widget and a state. One doesn’t go without the other. These can be declared in one file.

  • Don’t be afraid to lean on code generation for simple tasks. The fear of code generation often comes from seeing ginormous DI frameworks that take up just about all the logic of building the dependency tree for the app and handle it with code generation.

There’s absolutely no point in doing simple tasks manually. Code generation of data models, for example with json-serializable or other similar libraries, is what your project must have by default. 

Product quality

Both tips have to do with productivity because it affects the user experience the most.

  • Tip #1: Warm up the animations. For over a year, the Flutter community had been suffering from janky frames in animations on iOS — and not only iOS. Fortunately, long gone are the days when there was no way to fix it. There used to be all sorts of kluges, but they in no way looked like fundamental solutions.

Now we have one, so it absolutely doesn’t pay to forget about animation warm-up. You can even do it manually if your app isn’t covered by integration tests. However, writing some integration tests for that is not as hard as it seems either. So, don’t forget about the animation warm-up: you will actually see the effect.

  • Tip #2: if your animations are lagging, try rebuild stats. This doesn’t always work, but it has worked amazingly often for me. If you have trouble with performance on a screen, and you can see that the animations are lagging and aren’t working very smoothly, the first thing you have to try is Rebuild stats. Find out which widgets are redrawn hundreds and thousands of times—it’s clearly much too much. 

If it’s all okay visually, I recommend spending some time scrolling the screen and tapping the buttons. Sometimes it starts redrawing things after some time: for example, there are processes looped the wrong way in your code, triggering redraw events every single time.

If all the widgets work as they should and aren’t redrawn more often than they are supposed to, go to devtools and have a look at CPU-profilers.

We often see projects that go against the tips you’ve just read. More often than not, such projects turn into disappointments for their teams. They blame the technology, i.e., Flutter, for not being good enough, advanced enough, and ready enough to be used in a complex project.

My intention here was to bust this myth. Flutter is ready for tough challenges. But it’s all too easy to mess things up in a technology that hasn’t yet had much time to accumulate enough best practices. Hopefully, my tips will help you get the most максимум capabilities and benefits that Flutter could possibly give you.

To view or add a comment, sign in

More articles by Surf Flutter Team

Insights from the community

Others also viewed

Explore topics