Lessons Learned About Flutter Dart Null Safety
Jun 15, 2021
Towards the end of 2020 Flutter developers already knew that Dart null safety was coming. At the time I was on the Flutter SDK beta channel with my projects. I had a production release (build #30) after Christmas. Since I was getting production crashes and I wanted to stabilize the application before another large release I wanted to procrastinate the dealings with null safety after the next release. Decades of software engineering suggested a gut feeling that the null safety transition will induce a lot of code churn (boy I wish I knew how much right I was).
Then early 2021 while in Europe I added major features like KayakPro Genesis Port Smart Console support, FIT files and dozens of other significant hits. That alone caused enough refactoring and code change that I consciously was procrastinating the null safety embracement. Still one day in March I let myself lure into the transition because one of my plugins had an enhancement I craved for. I started to roll with it and ended up spending a whole day chasing null safety and at night I ended up miserably retreating from the effort (you’ll get some glimpse later why) and reverting the null safety changes. That was a big red alarm. By the way let me throw it out in advance: I think null safety is an amazing thing! The problem was that I realized that too many of my 40 plugin dependencies haven’t been converted to null safe versions.
- A third of them was null safe already
- Another third wasn’t yet null safe, but had null safe version in accessible development beta version
- A sizable portion of the plugins hadn’t have null safe version even in development released accessible version
You can ask me ‘Hey Csaba, why do you depend on so many plugins?’. That’s a great question and I’ve even seen a YouTube video where one piece of advice for advanced Flutter development was to not depend on too many plugins. My main principle is to not reinvent the wheel if I don’t have to. I want to focus on the business needs of my application and not spend time with the unnecessary details. If someone already solved a sub problem then it’s the wise choice to embrace that. Open source plugins have a better chance for issues being discovered and fixed and all kinds of enhancements contributed from the community. In my mind it’s not productive to cannibalize sources from plugins. I break that principle when a plugin does way more than I need and I only need an itsy-bitsy portion of the code, or I badly need a variation of the code. As a result, although my application is not too complex, it depends on 40 plugins. Of course there are downsides, but I’m willing to deal with that. One downside we see all over in this blog post when I need to deal with less maintained plugins.
Let me get back to my train of thoughts and address the last group in the list where there’s no null safe support even in beta versions. There can be several solutions:
- Search for an equivalent package which is better supported. This could be non trivial if the replacement package has a different API than the older one. Same API is smooth though, example could be uni_links vs. uni_link2 or data_connection_checker vs internet_connection_checker. In the uni_links case first I transitioned to uni_links2 but then over time uni_links caught up as well, so I reverted back. In the case of the internet_connection_checker the decision is more straightforward because data_connection_checker seems to be not maintained. Oftentimes nice maintainers mark packages discontinued explicitly, this can clear the question if it’s something maintained or not. More on that later.
- You may simply just wait if you can: supposedly by time the plugin authors have more and more pressure on them and may eventually take the time to upgrade. It can help if you open the git repository of the project, check if there’s already an issue for the null safety upgrade. If there is then give a thumbs up to the issue, otherwise raise an issue. You can also contribute (see later).
- You can look around in the list of forked repositories if any of them has a null safety modification done on them. Check if that fork has a PR back to the package’s repository and raise your voice for that PR. You have the ability to refer directly to a Git repository URL in the pubspec.yaml instead of a package name and version.
- You can fork a copy on your own and perform the null safety changes, and then you can replace. Make sure to open a PR against the upstream repo, so you contribute back to the community and most importantly hopefully by time you can get rid of your own fork.
Let’s stop for a second. You can say ‘Hey Csaba, null safety is optional! Why are you stressing yourself out?’. Well young Padawan, that’s in theory, but real life is different. After the failed null safety battle I mentioned above (in March) I kept postponing the update of my Flutter SDK all the way until bright day in the beginning of June. I suspected an update could produce a turd in the punch bowl. On said day I accidentally upgraded Flutter and my worst fear came true: it was indeed not optional for me any more to be null safe. A few 2+ deep transitive dependencies of the SDK itself collided with some of my 2+ deep transitive dependencies. Of course I straightened out intl package, integration_test package and aligned some other stuff but I had to realize that I was screwed. Believe me with all my two decades of experience that I was backed into a corner. My only choice was to take a deep breath, charge ahead and fight the null safety war. If I recall there were several of these transitive dependencies, one of which was related to crypto package but that wasn’t the only one. I started to adjust versions and seek for a treaty, but there wasn’t any resolution in the end. The battle lasted a whole week, let’s see why!
The toughest cases are packages which become discontinued and there’s no clear successor. I have several example for those:
- preferences became obsolete. I ran with the advised successor of pref, but that has a completely different API so I had a pretty major refactor step just because of that dependency. Usually there’s always an upside, for example pref supports slider type inputs natively, so I could convert my String persisted integer settings into native integers and their input became more natural (after some adjusting). But remember: this was just one of several application-wide refactoring (since I use settings options all around the code) due to null safety.
- charts_flutter was a major hurdle, it’s Google’s own charting library and it was not null safe. As you’ll see below I took over the task of converting more than half a dozen of my dependencies, but charting libraries have so many generics and dynamic behavior in nature that it’s out of my scope to tackle it. I had to look for another charting library. The new library has some upsides (automatically scales Y axis on the measurement screen as data flows in - this was user wish), although it has downsides too (does not support mirrored Y axis which I used for displaying pace based sport speeds). Now that I’m checking it for this blog post charts_flutter got a null safe version days ago (July 7th) however this was too late for me. Especially since there wasn’t any accessible development beta version so I had to move on. This required yet another major refactoring although I could throw out some code along the way.
Let’s talk about packages which I either converted (along with PRs back upstream), modified, or cannibalized. Once again, we talk about non trivial amount of work, working in alien code bases, new (to us) projects:
- bluetooth_enable: this plugin didn’t have null safety support, I performed the upgrade myself, provided a pull request to the author which got merged and hopefully a new version gonna be released. Currently I reference my own fork in my pubspec.yaml. I made even more contribution to that plugin by the way.
- fab_circular_menu: I’ve made slight contribution to this project before, but a major contribution is debugging and fixing issue #14 and issue #26 and providing a proper pull request for it. That pull request is still not merged although I need that change because I needed to increase the radius of the menu, so I ended up cannibalizing the code right now. This is a turn down.
- flutter_blue: this is a cornerstone plugin for my application since I’m communicating with all the fitness machines and heart rate monitors through Bluetooth Low Energy protocols. The plugin is null safe, however I see multiple crashes in production occurring constantly, so I ended up forking the plugin and perform modifications on my own. I’m depending on my own fork right now. The author seems to be not responsive to perform enhancements any more. Another turn down.
- flutter_brand_icons is officially discontinued. I only used the Strava logo from that plugin, so I ended up editing the font and keeping only the Fitbit, Flutter, Garmin, and Strava logos and cannibalizing the source. That turned out to be a great idea since the font size went from 334K to 2K, so a very nice size reduction.
- pref: as I mentioned in the previous list this was an API change compared to the predecessor plugin and I went through some struggles when refactoring my code but all turned out to be OK. I ended up contributing to this project too by devising a version of slider preferences where the slider is its own row so it can span out the whole screen.
- rw_tcx: this plugin is still not null safe but I cannibalized the source code. I chopped off all the fluff I didn’t need and made it capable of providing in-memory stream and also supporting gzip compression. Strava supports gzip compression for bandwidth savings and with in-memory files I don’t have to unnecessarily materialize any file which could even pose some permission issues.
- share_files_and_screenshot_widgets: this plugin is not null safe yet. Due to lack of time I only converted a portion of it. I cut down the features not used by me and I’m rolling with such a mutilated fork right now.
- spinner_input: I upgraded that plugin to be null safe and contributed it back to the author. The pull request is not merged yet, so yet again I’m depending on my own fork.
- strava_flutter: this plugin has the same author as the rw_tcx. This one is also not null safe and I cannibalized it for the same reasons as the TCX: I introduced gzip compression along with in-memory file handling.
- url_launcher: this plugin is part of the official Flutter plugins and I dedicate a separate blog post for it.
Wow, that list is actually long! I’ve done so much forensic work, flipping through a list of forks of various packages, studying PRs and determining healthiness. I’ve done my fair share of work as well in the form of large refactorings or converting packages to null safety myself. But realize that we haven’t even got to convert my own code yet!
Although this blog post may focus on difficulties and hurdles, inherently the sound null safety is an amazing thing. When you get to your own code you have to meticulously decide which object fields and code variables could be nullable or not. My main intent is always to try to be non nullable because that makes everything more straight forward, less nullability checks. Techniques:
- rely on late initialization
- initialize with a meaningful default value and then later modify
- nullability is only the last resort
During the conversion you have to revise most of your core business logic and answer difficult questions to yourself regarding the code paths. The end result is a more robust, more modern, and also somewhat quicker (according to some articles) application.
Flutter gave me back my inner child at the end of 2020, but some grievances gave me some taste of harsh reality.