7 tips for junior Flutter developers

7 tips for junior Flutter developers

Just starting with Flutter? These 7 tips can boost your learning process and make you more productive.

Featured on Hashnode

The beginnings in the world of programming are very hard, many things to learn, difficulties to understand the learning materials that one finds, doubts that appear and that seems impossible to solve...

This article stems from a Reddit thread where a user was asking for advice on how to improve his/her knowledge as a junior Flutter developer; I responded with what occurred to me at that time, and after responding I saw that perhaps it would be appropriate to delve a little deeper into this topic in a more didactic and orderly way:

  • Don't go crazy with state management
  • Understand how Flutter Widgets work
  • Use packages and plugins wisely
  • Add a static code analyzer
  • Accept that at some point you will have to learn native Android and/or iOS
  • Don't mix business and data logic in your presentation layer
  • Don't forget to add tests to your application

So without further ado, here are my suggestions for all of you who are starting to program applications with Flutter:

Don't go crazy with state management

For some strange reason, there are endless ways to manage the state in Flutter. While in other ecosystems such as Android or iOS we could enumerate the most used state management mechanisms in the industry with the fingers of one hand, in Flutter it seems that we would need the fingers of the whole body... and even then we would probably be missing some.

Before going into the details of how to approach this problem, let me list the traits that all software architectures should have:

  • The code should be easy to maintain, the changes needed to fix bugs should be easy to make
  • The code should be easy to extend, adding new features should not take too much effort
  • The code must be easy to test, it must be possible to add unit tests or any other type of test in a simple way
  • The code should be easy to package and distribute

Regardless of the approach used, if all these points are met, we can say that the architecture is good. Having that said if you don't know which state management mechanism to use, try a few and stick with the one that allows you to meet the above points. In addition, it is very important that you feel comfortable with it.

Personally, I have always used the most simple approach using provider explained in the official documentation to manage the state of my applications. It is extremely simple, but at the same time very flexible and powerful; I feel very comfortable working with it, and it allows me to develop very quickly, correct bugs easily, add tests easily, and publish my applications without complications.

If you have no idea where to start, I suggest you give it a try, and if you see that it doesn't work for you feel free to discard it and try other ways.

Understand how Flutter Widgets work

The Widget is the basic building block of Flutter. Every application is a big tree of hierarchical widgets, that is: a widget, inside another widget, and so on. A widget can contain an array of widgets within it. Being a declarative framework, Flutter manages state changes by redrawing the portion of the UI that has changed, that is, the widget whose state data has been modified, and therefore, all its children. Understanding this is critical to building apps that perform optimally, since it's our job as developers to minimize the impact of redraw cycles. A Flutter app that doesn't handle these loops well can suffer from performance issues, as well as laggy UI.

As always, I quote the official documentation for an introduction to these concepts. However, I would like to clarify some points that I consider to be key when we want to create an optimal visual interface that does not waste machine resources:

  • Use whenever possible StatelessWidget over StatefulWidget. If you're not sure which one you need, create a StatelessWidget and if at a certain point you see that you need to manage state, refactor it to StatefulWidget.
  • Divide as much as possible your UI into small widgets. This allows you to encapsulate your presentation logic, and improve performance since being small widgets, you will likely be able to use more StatelessWidgets.
  • Avoid creating functions that build widgets, instead create stand-alone widgets.

On this last point, I would like to clarify it with a code example:

class SampleLayout extends StatefulWidget {
  @override
  State<SampleLayout> createState() => _SampleLayoutState();
}

class _SampleLayoutState extends State<SampleLayout> {
  bool showBottomPortion = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          _getUpperPortion(),
          if (showBottomPortion) _getLowerPortion() else Container(),
          _getButton(),
        ],
      ),
    );
  }

  Widget _getUpperPortion() {
    return Expanded(
      child: Center(
        child: Text('Upper portion'),
      ),
    );
  }

  Widget _getLowerPortion() {
    return Expanded(
      child: Center(
        child: Text('Lower portion'),
      ),
    );
  }

  Widget _getButton() {
    return Expanded(
      child: Center(
        child: ElevatedButton(
          child: Text('Switch'),
          onPressed: () => setState(() {
            showBottomPortion = !showBottomPortion;
          }),
        ),
      ),
    );
  }
}

This block of code could be refactored as follows:

class SampleLayoutEnhanced extends StatefulWidget {
  @override
  State<SampleLayoutEnhanced> createState() => _SampleLayoutEnhancedState();
}

class _SampleLayoutEnhancedState extends State<SampleLayoutEnhanced> {
  bool showBottomPortion = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          _UpperPortion(),
          if (showBottomPortion) _LowerPortion() else Container(),
          _SwitchButton(
            onPressed: () => setState(
              () {
                showBottomPortion = !showBottomPortion;
              },
            ),
          ),
        ],
      ),
    );
  }
}

class _UpperPortion extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Center(
        child: Text('Upper portion'),
      ),
    );
  }
}

class _LowerPortion extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Center(
        child: Text('Lower portion'),
      ),
    );
  }
}

class _SwitchButton extends StatelessWidget {
  final VoidCallback onPressed;

  const _SwitchButton({required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Center(
        child: ElevatedButton(
          child: Text('Switch'),
          onPressed: onPressed,
        ),
      ),
    );
  }
}

In this second version we are using small independent widgets instead of using creation functions. This is a simple example where there really isn't much difference between using the first version and the second, but I think it illustrates well what I'm talking about.

Use packages and plugins wisely

One of the negative features of Dart regarding its dependency management is its inability to have two versions of the same dependency within the same application. This can lead to the so-called dependency hell in which the application is unable to compile due to transitive dependency incompatibilities. And what are transitive dependencies? Well, basically the dependencies of a dependency. That is, if your application depends on A, and A depends on B; then your application depends on A and B. And if it also depends on C, and C depends on B, then your application depends on A, B, and C. And what happens if A uses version 1 of B, but C uses version 2 of B? Your application won't compile in this case.

For this reason, and basically in order to reduce the complexity and size of your application, only use packages and plugins when it is really necessary. That doesn't mean that you have to reinvent the wheel, if there is a dependency that solves a problem for you, utilize it without detours; but if that problem is already solved by itself, it doesn't make sense to depend on something to solve it.

To give an example, the Flutter SDK already incorporates a mechanism to use the clipboard, if what you need is to simply paste data to the clipboard, use this system instead of depending on a third-party package or plugin.

If you think you're going to gain from using a plugin or package; choose one that is well documented and actively maintained. You can guide yourself by the ratings it has on pub.dev, I also suggest you to go to its GitHub page (or where it's hosted) and look at the frequency of its updates, how many people have participated in its creation, etc. Doing this can save you potential problems in the future.

Add a static code analyzer

Commonly called lint. A static code analyzer analyzes your code when it's not running, hence the use of the adjective 'static'. Its goal is to give you tips and tricks to:

  • Improve the performance of your application
  • Improve formatting and readability of your code
  • Warn you about possible code deprecations
  • Give you other tricks or good practices to improve your project in general

As of Flutter 2.8, the team added the flutter_lints package by default. Personally I have always used the lint package, but you can start with flutter_lints as it comes by default. When enabled, the lint tool searches the analysis_options.yaml file for custom rules. You will see warnings and information in your code highlighted by your IDE on how to improve its quality. I also recommend that you run flutter analyze to get a report on the current state of your project from time to time. If your Flutter app is mounted on a CI/CD system, running flutter analyze should be one of the pre-build steps.

Accept that at some point you will have to learn native Android and/or iOS

I understand that this statement might sound a bit scary to a beginner, but you have to understand that Flutter has its limitations; it runs on top of visual system components so we're going to be limited to those.

For example, if we want to program a process that runs in the background, we will not be able to do it with Flutter (precisely because it is limited to the visual layer). For example, if we want to work in the background in an android service we will have to write that logic in an android service and communicate it with our app via a platform bridge. In iOS, if we want to use the system extensions, we will also have to do it by writing platform code in Swift.

Do not worry about this if you are just starting with Flutter, just learn and if it comes to the point when you have a need that you cannot solve with Flutter, then you will have to spend time learning native programming. This includes learning a different language and a different SDK.

If the only thing we want is to create simple applications, it is unlikely that this scenario will occur, personally I believe that an engineer always has to look for the best way and tool to solve a problem, always with an energetic spirit and embracing new knowledge as a way of enrichment, not as an annoying obstacle. So if this scenario occurs, take it easy, without haste. After all, if you have chosen to learn to program, you will always have to be learning new tools and new ways of doing things.

Don't mix business and data logic in your presentation layer

This topic can lead to an entire article, but basically what we want to achieve is not to mix our business logic or data management with our presentation layer.

First, let's understand what each one corresponds to:

  • Business logic: these are the key algorithms that make our application add value to the user. It's not the visual part, it's not how we store data, it's not whether we use a database, none of that. To quickly identify this logic, think about if technology didn't exist, the problem you're trying to solve would have to be done by a person. The processes that this person would do is the business logic.
  • Data management: is the way we store, retrieve, edit and delete data. In this layer exists our requests to APIs, queries to databases, cache management...
  • Presentation logic: everything that has a visual representation. Here we have the Flutter widgets, as well as our state management classes (don't forget that state management exists because there is a visual layer, so state management is always going to be part of your UI layer).

There are several ways and architectures on how to approach this topic, and as I said at the beginning, it would lead to a single article exposing the details of this topic. To get you started easily, if you have to make API calls or access a database, create separate classes for it. If you want to dig deeper and learn about more advanced design patterns, I recommend you to look into Clean Architecture and the Repository Pattern.

Don't forget to add tests to your application

Automatic tests are essential in any application. Writing tests gives you the following advantages:

  • Speeds up the development of new features since it streamlines testing processes
  • Allows you to make refactors safely
  • It forces you to make your code readable and decoupled from other components

Again I refer to the official documentation for an overview of creating tests in Flutter. If you are starting now, I recommend that you spend some time first creating unit tests on your classes that do not inherit from a widget, for example on your BLoCs classes or your ViewModels, or other classes that only contain pure logic. Once you have mastered this you can move on to performing widget tests on your widgets; And finally, you can learn to create instrumentation tests on your application as a whole.

If what you are looking for is to get a job as a Flutter developer, rest assured that any serious company will require you to have a solid knowledge of testing, and I can assure you that mastering it will give you many points to be able to advance in the selection process.

Conclusion

So far the advice that I personally would give to a person who is starting with Flutter. If you would like me to elaborate on any of the points that I have mentioned please write it in the comments.

Thanks for reading this far, happy coding!


The cover image is derivated from a photo by Christopher Gower on Unsplash

Did you find this article valuable?

Support David Serrano by becoming a sponsor. Any amount is appreciated!