Solvro Talks - Flutter, Dart, and Dynamic Code Generation, or How We Write "Future" Code
Discover the fascinating world of Flutter and Dart with us in the latest Solvro Talk! We present dynamic code generation, or how to write applications "from the future." In our article, we delve into these technologies with the example of the ToPWR project—an innovative guide to Wrocław Tech. Discover how Flutter enables us to create cross-platform applications from a single codebase and how Dart ensures safe and efficient programming. Find out why, when developing applications at Solvro, we write code that not only meets current needs but also future challenges.
Our project: ToPWR
The following Solvro Talk is a presentation from the project team creating the ToPWR mobile app, a mega super cool handy guide to Wrocław Tech. An essential for every student and more :). The app is currently being developed fully open-source and is nearing release for the entire Wrocław Tech community (at the latest within 2-3 months—it will definitely be ready to install for the next semester). I'm excited to present some interesting technologies that we are using in our project, and for anyone interested, I refer you to our repository to leave us a 🌟 star 🌟 on GitHub!
What is Flutter?
Open-source Framework
Flutter is a fully open-source framework, created by Google since 2017.
Multiplatform, single codebase
It allows creating cross-platform applications from a single codebase.
Natively compiled
Applications compile to machine code, native to the target environment - which makes them fast ⚡ like Lightning McQueen.
Multiplatform
Mobile Framework
Flutter began as a purely mobile framework. The idea was simple: You write a single codebase and compile it into an app for both Android and iOS. This eliminates the need to maintain two separate development teams writing exactly the same product, but for two platforms. Thus, using similar resources, we can release an application twice as fast.
Not just mobile
"Mobiles" are still an area where Flutter feels most comfortable. However, now by writing a single source code in Flutter, we can compile the app to almost any platform:
-
- Web applications
- Desktop applications for Windows, Linux, and MacOS.
- And even embedded systems (embedded systems), for example, for Toyota's embedded computers.
At the time of writing this article, all these platforms already have stable support on the `stable` distribution channel. When I started my journey with this technology, stable support was only for mobile applications, and web support was entering the `beta` channel. Reasonable support for desktop applications was still a thing of the future.
However, over these few years, Flutter has evolved significantly and matured, supporting all popular platforms stably. The power of open-source is also evident here, even though Flutter is developed mainly by the giant corporation Google, it also accepts external contributors. It could even be you, but also companies often downright competitive to the Mountain View Company. The best example is Microsoft, which officially and actively collaborated on creating support for Windows.
Flutter octopus
If you want to see Flutter's multi-platform capability in action, I have a one-minute clip from the Flutter Product Keynote 2019 "Flutter octopus." On the clip, you can see the Flutter Counter App (a sort of Hello World for Flutter), being debugged simultaneously on 7 different devices with different platforms and operating systems from a single codebase.
At the end of the clip, Zoe changes the color in the app from blue to red. And what? The apps rebuild, reflecting the change, but they don't lose state—the counter still has the same value as before. This smoothly leads us to Hot Reload—considered by some as the greatest advantage of Flutter.
Statefull Hot Reload
Another, 20-second clip about hot reload:
The video briefly presents how hot reload works, where relatively simple changes are injected on-the-fly into the running Dart VM—Dart's virtual machine. Dart is the programming language we use to write Flutter applications. But hold on! What virtual machine? I mentioned that the applications are natively compiled and have fast binaries.
Natively fast
The solution to this puzzle is quite simple. Dart and Flutter have two types of compilation: Just-in-time for debug builds—when developing our application, we use a developer toolset and can take advantage of hot reload and other convenient features. However, when we want to release our creation to the world, we switch to a release build which is compiled Ahead-of-time to native code for a particular processor, optimized purely for performance and speed.
As we see in the picture, for all platforms except the Web, they compile directly to assembler binaries. However, due to technological limitations, web applications compile to JavaScript, creating a rather "heavy" single-page frontend application. This makes it the slowest target platform for Flutter.
Nevertheless, a beacon of hope is the support for compilation to WebAssembly, which entered the stable version of Flutter last month. However, such compiled applications currently require a fairly new version of a chromium-based browser: https://docs.flutter.dev/platform-integration/web/wasm.
However, reportedly such compiled applications are faster and may eliminate that last performance bottleneck Flutter somewhat suffers from with web applications.
No more unnecessary repetition
Attached is a "confession" from Flutter's official site and, as a light-hearted joke, my version under which I can sign as Techlead in the ToPWR team.
However, it is not exaggerated. You write one code, placing the simplest `if` if you want to slightly change behavior for a particular platform. We also add various widgets which ensure responsiveness and, for example, handle keyboard shortcuts on the desktop.
For "close-to-platform" functionality, you can write certain logic fragments or views in native technology, e.g., native Android (Java/Kotlin), native iOS (Objective-C/Swift), Javascript for the Web, and C++ for desktop applications. You can wrap such native fragments in Dart API and release them as a plugin on pub.dev. And probably in most cases, someone has already done it before you :)
Everything’s a widget!
In Flutter, almost every UI element is a widget, which can be freely nested and combined. There are also many ready-made (official) widgets implementing well-known elements from popular design systems, e.g., Material Design familiar from Android Material Design, Cupertino widgets known from iOS Cupertino widgets, or Microsoft's Fluent UI
It's very easy to create your own widgets, which are easy to parameterize and use in many parts of the application, or even in many applications! On every platform. No problem!
Example widget, which we'll return to a few times:
Dart and its sound null safety
Dart is the programming language, partially used in Flutter and which we utilize to create applications in Flutter. For this presentation, I'll talk about a particular property of its type system, specifically null-safety. It's a concept found in at least a few modern programming languages, but maybe you haven't heard about it yet.
Non-nullable types
Variables cannot have a `null` value by default. If at any point in the code we perform an operation that attempts to assign even a potentially null value, the entire application will not even compile. We can only assign such a value if we first ensure that it does not contain null. Dart's type system is "sound," meaning if something has the type `int` it will always be an integer and cannot be anything else.
Nullable types
If we want to store null values somewhere, we need to explicitly allow for it by adding a question mark to the type. But then, if we want to use this variable somewhere where null is not allowed, we first need to check if it is null—otherwise, the application will not even compile. By using null safety, you'll never encounter a run-time error related to a null value being where it shouldn't be.
Null safety - example
Returning to our widget. It is an ordinary class in Dart that extends `StatelessWidget`. In the highlighted fragment, we have defined class fields with types and a constructor that accepts them. As we can see, `title` must always be a string, while `actionTitle` can be a string or null, because sometimes we may not want it and simply won't display the related UI fragment.
And when using our widget and we try to insert something into our `title` that potentially could be null—we will immediately get an error.
The problem is `context.localize`, which has a nullable type, meaning it can be null. By using the `?.` operator, we extract the departments attribute from it only if it isn't null. However, the entire expression can still be null, while our title cannot.
Time to fix it!Let's add a default value that will be inserted instead of null in this place using the operator: `??`:
Type System in Dart - Advantages
Compile-time Safety
Complete elimination of type-related runtime errors.
Easier Code Changes
When making changes, the type system monitors almost all other places that require changes.
Faster Binaries
More efficient ahead-of-time compilation.
Dynamic Code Generation
By definition, this is "the ability of a runtime environment or development framework to automatically generate and update source code before its compilation/execution". What does this mean in practice? I have as many as 5 examples for you!
Example 1 - UnmodifableEnumMap
Let's assume we have a simple enumeration that defines tabs in a TabView:
Our Code
And we want to associate them with widgets representing appropriate views. We can do this very securely using the `@unmodifableEnumMap` annotation.
Next, we need to run our build_runner generator with a simple command in the terminal:
build_runner
This starts a process that finds all elements marked by various generative libraries and generates the necessary code fragments in adjacent files with the extension `.g.dart`.
If we want the build_runner to run automatically after each change in such a file, use the `watch` option instead of `build`:
Generated Code
Usage
This generates a new implementation of `Map`, ensuring that each enumeration value has an assigned value. For example, each tab has its widget/view:
What if we add a new tab?
For example, a new parking tab? (YES - THERE WILL BE ONE IN ToPWR)
Compilation Error!!!
We Are Saved!
The generator did not allow us to build the app with a missing parking widget, we can quickly fix this:
Example 2 - freezed
Another example is the @freezed annotation, which turns a class with only constructors into a class known in many languages as a "data class." A freezed class is an immutable class that overrides the equality operator and hashCode, so two classes of this type are equal if and only if all its attributes given in the constructor are equal. Additionally, freezed adds serialization toJson() and fromJson() - allowing serialization without using "magic strings".
Official example from the freezed documentation:
Freezed - boredapi.com
Here is my example of an activity model from https://www.boredapi.com/api/activity
Example 3 - Riverpod
Riverpod is a state management library that uses code generation to minimize its boilerplate as much as possible.
These two lines + a few-line model from the previous example allow us to fetch data from an API, parse it into safely typed models, and thanks to riverpod we can fetch this data directly in the presentation layer and display it. Riverpod takes care of refreshing, wrapping asynchronous values into a safe `AsyncValue` - catching all sorts of errors for us and specially wrapping them. It also returns a special value during loading and allows implementing pull-to-refresh in one line.
More about riverpod: https://riverpod.dev/
Example 4 - Riverpod cont.
The second example with riverpod, this time the class of a controller for a familiar tab (NavBarEnum). In a few lines, we have implemented a controller with automatic UI refreshing if values change as a result of invoking the `goTo` method:
Usage of Controller (familiar code):
This is how listening to changes in the UI layer looks, which will be automatically rebuilt if the tab changes:
EVERY LINE OF CODE that we have to write ourselves is presented. The rest will be generated by build_runner.
Example 5 (last) graphql_codegen
In ToPWR, we use the GraphQL API instead of REST. This is very convenient thanks to this library. GraphQL is inherently typed, each endpoint returns a file of the type GraphQL Schema Definition Language (SDL), which provides a typed definition of all models and available operations: https://graphql.org/learn/schema/
Our library uses code generation and this fetched file to automatically generate safely typed models and operations, using only SDL and our simple textual GraphQL query:
Thanks to graphql_codegen, communication with the API is even simpler and safer in terms of types.
Code Generation Advantages
In summary, code generation provides the following advantages:
Even Greater Compile-time Safety
Possible additional enforcement of types or other conditions in some dangerous cases.
Reduction of Boilerplate
Shorter and more meaningful code, while boilerplate writes itself
Less Prone to Human Error
Automating repetitive tasks
Thanks for Reading!
If you enjoyed this article, leave a star on our repo: https://github.com/Solvro/mobile-topwr
Check out our other projects at KN Solvro: https://solvro.pwr.edu.pl/en/portfolio
Also consider joining our club by contacting us: https://solvro.pwr.edu.pl/en/kontakt or waiting for the next semi-annual recruitment campaign. To not miss anything, follow us on Facebook: facebook.com/knsolvro or LinkedIn: linkedin.com/company/knsolvro
If you have any questions about Flutter, don't hesitate to reach out at https://solvro.pwr.edu.pl/en/kontakt, email kn.solvro@pwr.edu.pl, or directly to Szymon Kowaliński: https://kowalinski.dev/
Can't wait to see the amazing apps you'll create with Flutter! Who knows, maybe it will be an app that we create together at KN Solvro!