The way mobile apps are being built is shifting. We’re seeing an overall transition from imperative to declarative paradigms taking hold within the industry, particularly when it comes to UI frameworks. Apple’s SwiftUI, Android’s JetPackCompose, and countless others join the ranks in building a future that promises to dramatically increase developer productivity, enabling teams to build apps faster and with less code.
Perhaps one of the most compelling – and certainly among the fastest-growing – UI frameworks out there is Flutter. As with other declarative systems, there are both benefits and drawbacks to working with Flutter. For developers who have predominately built UI in an imperative style, there’s also a mindset shift to embrace when adopting something like Flutter. Read on to learn more about this movement, its pros and cons for devs, and how Flutter, in particular, approaches its declarative structure.
Benefits of declarative programming
Declarative paradigms create an added layer of abstraction for the human programmer, taking care of many of the primitive control flow elements that are the building blocks of any code. While imperative systems require a developer to write exactly how they want a program to execute a task with step-by-step instructions (for loops, while loops, if-else statements, i.e. “traditional” programming), declarative paradigms provide an added framework that abstracts these things away. A dev can now focus more on what the end result of a code block must be rather than how it should be executed.
Mobile teams building an app can benefit significantly from declarative frameworks for a few reasons:
Code efficiency and readability
This is perhaps the most obvious benefit of using a declarative system. You can achieve the same results with many fewer lines of code, ultimately making your program shorter. Additionally, because you’re relying on an underlying framework to determine the execution, the lower-level control flow is already optimized for efficiency, so your code won’t fall victim to excessive loops within loops. This sheer complexity reduction means fewer places for bugs to hide and makes your code more human-reader-friendly, too.
This concept is defined as, “the ability to replace an expression with its result without changing the meaning/behavior of the program.” Essentially, referential transparency means that functions do not produce “side effects” or modifications to other values outside of their local scope. This quality of declarative paradigms gives you ultimate predictability to test and plan your program’s behavior and minimizes needing to handle state manually.
Immutability of objects and variables
Languages and frameworks that are entirely declarative – as opposed to multiparadigm ones like Python – rely on programming with immutables, meaning their value cannot be changed. Depending on what you’re trying to do, this might actually be a disadvantage. But in the context of building UI, this can be a huge benefit because it helps avoid race conditions. In multithreaded apps, immutability ensures that a thread is safe to act out its functions on an object without the risk of that object’s value being changed by other threads. It, therefore, provides both safety and consistency for your app’s experience.
Because declarative paradigms abstract away lower-level workings, they’re often safer in the long run if the databases you rely on change. A great example of this is the relational database (RDB), which gives the end user a simple interface language (SQL) to query their data while hiding all the work that must be done to traverse a database for the record you need. If you’ve written imperative code to traverse, loop through, sort, etc., a database, and the structure of that data changes, your code no longer works. With declarative solutions like SQL, the end user is protected from those changes in the long run because the underlying RDB adapts instead.
A huge boon for teams looking to build apps quickly is the time-saving element of declarative frameworks. Depending on the framework, many standard functions, operations, classes, and other data structures have already been built and are there for you to use. You save time by focusing on your end result and the parameters you’re inputting rather than reinventing the wheel.
Related to the efficiency point above, using a declarative paradigm can make your program perform better because the framework itself optimizes control flow, as well as how to handle things like memory access and management. An analysis of standard code metrics (lines of code, number of statements, cyclomatic complexity, and cognitive complexity) for a few problems solved in both styles found that declarative solutions performed better across almost all metrics.
Drawbacks of declarative programming
Declarative frameworks are not suitable for all use cases, and they certainly come with disadvantages – especially for those used to writing more traditional programs. These include:
The complexity of abstraction
A high level of abstraction characterizes declarative code. While this abstraction enables developers to represent complex programs in a compressed form, the drawback is that the more sophisticated the application, the greater the danger that the code becomes convoluted and can only be read or edited by the developer who originally wrote it. Abstraction challenges companies that want to maintain and develop applications without relying on a single person’s knowledge.
A different conceptual model
People tend to think of processes moving towards a goal rather than starting from a goal and working backward. Declarative solutions require developers to rethink and accustom themselves to that concept. This might initially slow down problem-solving as you adjust to a declarative model, but it often becomes second nature the longer you work with it.
Lack of control
The imperative vs. declarative comparison is an example of the age-old debate of freedom vs. safety. While declarative frameworks can cut back the risk of buggy code because a lot of components are pre-built, they remove a huge element of control from you as a developer to customize the conditions around a program you’re building. Some languages, as we mentioned above, do support a multiparadigm approach. C++, for example, has been greatly modernized by the addition of the Standard Template Library, which provides algorithms, containers, functions, and iterators to pull from.
Longer debugging process
Though not always the case, debugging programs built with declarative frameworks can take a longer time. It can be more difficult to trace an error through not only your own code but the library from which you pulled a function or algorithm. You’ll also get bigger stack traces when using a tool to debug.
Flutter as a declarative framework
The overall benefits and drawbacks of declarative programming apply to Flutter, of course, but there are specific things about this framework that are important to look at. If you’re adopting Flutter from a background of imperative UI programmings, such as Android SDK or iOS UIKit, here are a few things to consider:
You’ll be rebuilding parts of your UI entirely instead of mutating them
If you’re coming from an imperative style of building UI, you might be used to constructing an entire UI entity and mutating it with methods and setters when it needs to change. This is not the case with Flutter. Instead, the developer describes what the UI should look like for a given state, and the framework creates it using sensible defaults and context. A state change triggers a rebuild of the entire UI. This is ok – modern CPUs on Flutter are fast enough to do so and can even handle animations. What’s more, a UI rebuild removes a whole category of state-related bugs and makes it easier to conduct in-app updates.
View configurations are immutable, while elements and rendering objects are mutable
We mentioned above that declarative paradigms favor immutability. You can see this in action by looking at widgets, which are the building blocks of UI for Flutter. When the UI changes, a widget does not get modified – rather, it triggers an entire rebuild of itself and constructs new widget instances in the form of a subtree. Widgets, by nature, are temporary objects that are merely blueprints of information – which makes them inexpensvie to rebuild. On the other hand, Elements and RenderObjects in Flutter are mutable. Elements are the main class that represents what is actually seen on a device screen – an instantiation of a widget at any point in the tree. RenderObjects are the layers of code that actually manage layout and rendering. These include RenderBox, RenderFlex, and RenderOpacity. Deep inside the framework’s code for RenderObjects, there are methods that actually implement the rendering change that you, as a developer, instructed by inputting the parameters into your widget.
Navigation in Flutter is declarative, starting with 2.0
In the early days, Flutter adopted an imperative style of navigation. This provided ways to mutate the stack of widgets via push( ) and pop( ) methods and, therefore, only provided control access to add/remove a page from the top of the stack. Flutter 2.0 introduced declarative navigation. This allows developers to control the navigation stack completely (not just add/remove from the top of the stack) with a new declarative API design that works in tandem with the existing Navigator 1.0.
When it comes to building beautiful mobile UI with efficiency, Flutter is a great option. Its widespread adoption is a testament to that. If you’re using Flutter (or considering it), check out our new SDK to help monitor the health and stability of your mobile app. Get started for free.