Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Flutter Design Patterns and Best Practices

You're reading from   Flutter Design Patterns and Best Practices Build scalable, maintainable, and production-ready apps using effective architectural principles

Arrow left icon
Product type Paperback
Published in Sep 2024
Publisher Packt
ISBN-13 9781801072649
Length 362 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Jaime Blasco Jaime Blasco
Author Profile Icon Jaime Blasco
Jaime Blasco
Daria Orlova Daria Orlova
Author Profile Icon Daria Orlova
Daria Orlova
Esra Kadah Esra Kadah
Author Profile Icon Esra Kadah
Esra Kadah
Arrow right icon
View More author details
Toc

Table of Contents (19) Chapters Close

Preface 1. Part 1: Building Delightful User Interfaces
2. Chapter 1: Best Practices for Building UIs with Flutter FREE CHAPTER 3. Chapter 2: Responsive UIs for All Devices 4. Part 2: Connecting UI with Business Logic
5. Chapter 3: Vanilla State Management 6. Chapter 4: State Management Patterns and Their Implementations 7. Chapter 5: Creating Consistent Navigation 8. Part 3: Exploring Practical Design Patterns and Architecture Best Practices
9. Chapter 6: The Responsible Repository Pattern 10. Chapter 7: Implementing the Inversion of Control Principle 11. Chapter 8: Ensuring Scalability and Maintainability with Layered Architecture 12. Chapter 9: Mastering Concurrent Programming in Dart 13. Chapter 10: A Bridge to the Native Side of Development 14. Part 4: Ensuring App Quality and Stability
15. Chapter 11: Unit Tests, Widget Tests, and Mocking Dependencies 16. Chapter 12: Static Code Analysis and Debugging Tools 17. Index 18. Other Books You May Enjoy

Understanding the difference between declarative and imperative UI design

The beauty of technology is that it evolves with time based on feedback about developer experience. Today, if you’re in mobile development, there is a high chance that you have heard about Jetpack Compose, SwiftUI, React Native, and of course Flutter. The thing these technologies have in common is both that they’re used for creating mobile applications and the fact that they do it via a declarative programming approach. You may have heard this term before, but what does it actually mean and why is it important?

To take full advantage of a framework, it’s important to understand its paradigm and work with it rather than against it. Understanding the “why” behind the architectural decisions makes it much easier to understand the “how,” and to apply design patterns that complement the overall system.

Native mobile platforms have a long history of development and major transitions. In 2014, Apple announced a new language, Swift, that would replace the current Objective-C. In 2017 the Android team made Kotlin the official language for Android development, which would gradually replace Java. Those introductions had a hugely positive impact on the developer experience, yet they still had to embrace the legacy of existing framework patterns and architecture. In 2019, Google announced Jetpack Compose and Apple announced SwiftUI – completely new toolkits for building UIs. Both SwiftUI and Jetpack Compose take advantage of their respective languages, Swift and Kotlin, leaving legacy approaches behind. Both toolkits also loudly boast their declarative programming paradigm. But language advantages aside, let’s explore why declarative is now the industrial de facto and what is wrong with imperative.

Understanding the imperative paradigm

By definition, the imperative programming paradigm focuses on how to achieve the desired result. You describe the process step by step and have complete control of the process. For example, it could result in code such as this:

fun setErrorState(errorText: String) {
    val textView = findViewById<TextView>(R.id.error_text_view)
    textView.text = errorText
    textView.setTextColor(Color.RED)
    textView.visibility = View.VISIBLE
    val button = findViewById<Button>(R.id.submit_button)
    button.isEnabled = true
    val progressView = findViewById<ProgressBar>(R.id.progress_view)
    progressView.visibility = View.GONE
}

In the preceding snippet, we imperatively described how to update the UI in case of an error. We accessed the UI elements step by step and mutated their fields.

This is a real example of code that could’ve been written for a native Android application. Even though this approach may be powerful and gives the developer fine-grained control over the flow of the logic, it comes with the possibility of the following problems:

  • The more elements that can change their presentation based on a state change, the more mutations you need to handle. You can easily imagine how this simple setErrorState becomes cumbersome as more fields need to be hidden or changed. The approach also assumes that there are similar methods for handling a progress and success state. Code such as this may easily become hard to manage, especially as the amount of views in your app grows and the state becomes more complex.
  • Modifying the global state can produce side effects. On every such change, we mutate the same UI element and possibly call other methods that also mutate the same elements. The resulting myriad of nested conditionals can quickly lead to inconsistency and illegal states in the final view that the user sees. Such bugs tend to manifest only when certain conditions are met, which makes them even harder to reproduce and debug.

For many years, the imperative approach was the only way to go. Thankfully, native mobile frameworks have since started adopting declarative toolkits. Although these are great, developers who need to switch between paradigms inside of one project can encounter many challenges. Different tools require different skills and in order to be productive, the developer needs to be experienced with both. More attention needs to be paid to make sure that the application that is created with various approaches is consistent. While the new toolkits are in the process of wider adoption, some time and effort are required until they are able to fully implement what their predecessors already have. Thankfully, Flutter embraced declarative from the start.

Understanding the declarative paradigm

In an imperative approach, the focus is on the “how.” However, in the declarative approach, the focus is on the “what.” The developer describes the desired outcome, and the framework takes care of the implementation details. Since the details are abstracted by the framework, the developer has less control and has to conform to more rules. Yet the benefit of this is the elimination of the problems imposed by the imperative approach, such as excessive code and possible side effects. Let’s take a look at the following example:

Widget build(BuildContext context) {
     final isError = false;
     final isProgress = true;
     return Column(
      children: [
        MyContentView(
          showError: isError,
        ),
        Visibility(
          visible: isProgress,
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
      ],
    );
}

In the preceding code, we have built a UI as a reaction to state changes (such as the isError or isProgress fields). In the upcoming chapters, you will learn how to elegantly handle the state, but for now, you only need to understand the concept.

This approach can also be called reactive, since the widget tree updates itself as a reaction to a change of state.

Does Flutter use the declarative or imperative paradigm?

It is important to understand that Flutter is a complex framework. Conforming to just one programming paradigm wouldn’t be practical, since it would make a lot of things harder (see https://docs.flutter.dev/resources/faq#what-programming-paradigm-does-flutters-framework-use). For example, a purely declarative approach with its natural nesting of code would, make describing a Container or Chip widget unreadable. It would also make it more complicated to manage all of their states.

Here’s an excerpt from the build method of the Container describing how to build the child widget imperatively:

  @override
  Widget build(BuildContext context) {
    Widget? current = child;
    // ...
    if (color != null) {
      current = ColoredBox(color: color!, child: current);
    }
    if (margin != null) {
      current = Padding(padding: margin!, child: current);
    }
    // ...
}

Even though the main approach of describing the widget tree can be viewed as declarative, imperative programming can be used when it feels less awkward to do so. This is why understanding the concepts, patterns, and paradigms is crucial to creating the most efficient, maintainable, and scalable solutions.

If you are coming from an imperative background, getting used to the declarative approach of building the UI may be mind-bending at first. However, shifting your focus from “how” to “what” you’re trying to build will help. Flutter can help you too, as instead of mutating each part of the UI separately, Flutter rebuilds the entire widget tree as a reaction to state changes. Yet the framework still maintains snappy performance, and developers usually don’t need to think about it much.

In the next section, let’s take a closer look at the abstraction to understand how the what actually works. We will explore not only how to use the widgets as a developer but also how the framework efficiently handles them under the hood. We will cover what to do and what not to do to avoid interfering with the building and rendering processes.

You have been reading a chapter from
Flutter Design Patterns and Best Practices
Published in: Sep 2024
Publisher: Packt
ISBN-13: 9781801072649
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image