🗂 Decomposing theme elements styles in your Flutter app
Tags: dart, flutter, ios, android
Published: 2021-11-22 11:46:00

## Introduction

Hey-hey, DEV friends! 😉

Lately, I've been reading and refactoring other people's code more and more often. Both incredibly beautiful and… the kind of code I'd like to take apart with you today. 

> 💡 **Disclaimer:** This article does not claim to be the ultimate truth. The method of code decomposition shown below is just an example, you can always write _how you do it_ in the comments below. Feel free, it will be interesting to read!

Great, let's do it! 👇

<a name="toc"></a>
### 📝 Table of contents

- [How task decomposition helps us](#how-task-decomposition-helps-us)
  - [Note about the decomposition in Flutter](#note-about-the-decomposition-in-flutter)
  - [Example of architectural plan](#example-of-architectural-plan)
- [Problems with code without decomposition](#problems-with-code-without-decomposition)
- [How I do it](#how-i-do-it)

<a name="#how-task-decomposition-helps-us"></a>
## How task decomposition helps us

Let's begin by defining what decomposition from [Wiki](https://en.wikipedia.org/wiki/Decomposition_(computer_science)):

> _[...] Decomposition in computer science, also known as factoring, is breaking a complex problem or system into parts that are easier to conceive, understand, program, and maintain._

Let's turn to a simple diagram to better understand the principles of tasks decomposition. Take the launch of the “Awesome App” to market as an example. 

Without decomposing this task, its diagram would look something like this:

![without decomposition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jqu4uicfnz83l8qpabx8.png)

Where do you need to start, and how do you come to launch? Only the desired result without detailing the process itself. Nothing is clear… But if we apply even basic knowledge of decomposition, the diagram will look something like this:

![with decomposition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q0ddrferb8q8qbha4cx1.png)

Much better, isn't it? 🤓 We've broken down the process of launching an application into big blocks with clear actions: research, design, writing code, testing, and launching.

Next, we can break these blocks into smaller ones, and so on, until we get to atomic tasks (_which can no longer be broken down into smaller ones_).

[↑ Table of contents](#toc)

<a name="#note-about-the-decomposition-in-flutter"></a>
### Note about the decomposition in Flutter

I would like to note that the code written on the multiplatform [Flutter](https://flutter.dev) framework, **like no other**, requires constant refactoring and decomposition almost from the beginning of the project. 

All because the framework has been designed to write code in declarative style, where every element is a separate widget. And when the code becomes a lot (_for example, starting from the 5th degree of nesting_), it is simply impossible to read it without effort.

That's why you should always have an **architectural plan for your application**, which you will stick to most of the time. In my opinion, this is both a plus and a minus at the same time. Otherwise, maintaining and further developing the project inevitably turns into a _natural_ pain.

[↑ Table of contents](#toc)

<a name="#example-of-architectural-plan"></a>
### Example of architectural plan

What is an “architectural plan” for writing software? Simple: it is a set of rules that **all** programmers follow when writing code. If you're familiar with a frontend development (React, Vue, etc.), here's a sample set of rules for writing project code:

- ✅ All components must be stored in the `./src/components` folder with easy-to-understand names.
- ✅ All helper functions must have a pointer to the action in their name (for example, if the helper get a link to the user's avatar, its name could be `getUserAvatarURL`).
- ✅ The maximum number of props in a component should not exceed 5.
- ✅ …and many more!

Do you feel like there's no more chaos in your head when you have rules like that? That's the beauty of an architecture plan for software! You just have to follow the accepted rules for a particular project. That's it.

> ⚡️ **Warning:** Please don't confuse an architecture plan with a style guide or language/framework best practices! The architecture plan is more like a local checklist for a particular project, than a globally accepted norms and standards.

[↑ Table of contents](#toc)

<a name="#problems-with-code-without-decomposition"></a>
## Problems with code without decomposition

I was recently doing a review of a project that was written in Flutter and found a widget like this:

```dart
// ./lib/screens/app.dart

/// Application widget.
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      debugShowCheckedModeBanner: false,
      // 👇 start default theme (light)
      theme: theme: ThemeData().copyWith(
        textTheme: GoogleFonts.interTextTheme(
          Theme.of(context).textTheme,
        ).copyWith(
          bodyText2: TextStyle(color: BrandColor.secondary[700]),
          subtitle1: TextStyle(color: BrandColor.secondary[700]),
          headline4: TextStyle(color: BrandColor.primary[500]),
        ),
        scaffoldBackgroundColor: BrandColor.secondary[500],
        primaryColor: BrandColor.primary[500],
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(
            primary: BrandColor.primary[500],
          ),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            onPrimary: Colors.white,
            primary: BrandColor.primary[500],
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(16.0),
            ),
            padding: const EdgeInsets.symmetric(vertical: 18.0),
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            primary: BrandColor.primary[500],
            side: BorderSide(
              color: BrandColor.primary[500] as Color,
            ),
          ),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          backgroundColor: BrandColor.secondary[300],
          showSelectedLabels: false,
          showUnselectedLabels: false,
          selectedIconTheme: const IconThemeData(size: 26.0),
          unselectedIconTheme: const IconThemeData(size: 26.0),
          selectedItemColor: BrandColor.primary[500],
          unselectedItemColor: BrandColor.primary[300],
        ),
      ),
      // 👇 start dark theme
      darkTheme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: BrandColor.secondary[700],
        primaryColor: BrandColor.primary[500],
        textTheme: GoogleFonts.interTextTheme(
          Theme.of(context).textTheme,
        ).copyWith(
          bodyText2: TextStyle(color: BrandColor.secondary[500]),
          subtitle1: TextStyle(color: BrandColor.secondary[500]),
          headline4: TextStyle(color: BrandColor.primary[500]),
        ),
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(
            primary: BrandColor.primary[500],
          ),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            onPrimary: Colors.white,
            primary: BrandColor.primary[500],
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(16.0),
            ),
            padding: const EdgeInsets.symmetric(vertical: 18.0),
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            primary: BrandColor.primary[500],
            side: BorderSide(
              color: BrandColor.primary[500] as Color,
            ),
          ),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          backgroundColor: BrandColor.secondary[900],
          showSelectedLabels: false,
          showUnselectedLabels: false,
          selectedIconTheme: const IconThemeData(size: 26.0),
          unselectedIconTheme: const IconThemeData(size: 26.0),
          selectedItemColor: BrandColor.primary[500],
          unselectedItemColor: BrandColor.primary[300],
        ),
      ),
      themeMode: ThemeMode.system,
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (_) => const SplashScreen(),
        '/sign-in': (_) => const SignInScreen(),
        '/main': (_) => const MainScreen(),
        // ...
      },
    );
  }
}
```

And that's not counting the imports, which take up the first 10 lines! It's already quite difficult to read or make edits to such code. 

But this massive code can easily be decomposed! 🚀

[↑ Table of contents](#toc)

<a name="#how-i-do-it"></a>
## How I do it

1️⃣ First, you need to identify the most appropriate elements for it. 

One of the most eye-catching parts of the code is the code section defining the application's design themes: the light and dark themes. 

2️⃣ Second, let's think about how we're going to do this. 

I think it will be enough to put the styles in separate functions (for example, `lightThemeData()` and `darkThemeData()`), which will be available in the context of each widget.

3️⃣ Third, for easier access to styles out of context, I will use the [extension methods](https://dart.dev/guides/language/extension-methods) (a built-in Dart language functionality). If you aren't yet familiar with it, I suggest you watch this short video by Filip Hracek:

{% youtube D3j0OSfT9ZI %}

> 📌 **Note:** Furthermore, I usually store the styles in the `./lib/styles` folder, but you can name it whatever you like.

Sounds like a good plan! Let's rewrite the code. Starting with methods for the class with styles:

```dart
// ./lib/styles/themes.dart

/// Theme data for project.
extension BrandThemeData on BuildContext {
  /// Styles for the light theme.
  ThemeData lightThemeData() {
    return ThemeData().copyWith(
      textTheme: GoogleFonts.interTextTheme(Theme.of(this).textTheme).copyWith(
        bodyText2: TextStyle(color: BrandColor.secondary[700]),
        subtitle1: TextStyle(color: BrandColor.secondary[700]),
        headline4: TextStyle(color: BrandColor.primary[500]),
      ),
      scaffoldBackgroundColor: BrandColor.secondary[500],
      primaryColor: BrandColor.primary[500],
      textButtonTheme: TextButtonThemeData(
        style: TextButton.styleFrom(
          primary: BrandColor.primary[500],
        ),
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          onPrimary: Colors.white,
          primary: BrandColor.primary[500],
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16.0),
          ),
          padding: const EdgeInsets.symmetric(vertical: 18.0),
        ),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: OutlinedButton.styleFrom(
          primary: BrandColor.primary[500],
          side: BorderSide(
            color: BrandColor.primary[500] as Color,
          ),
        ),
      ),
      bottomNavigationBarTheme: BottomNavigationBarThemeData(
        backgroundColor: BrandColor.secondary[300],
        showSelectedLabels: false,
        showUnselectedLabels: false,
        selectedIconTheme: const IconThemeData(size: 26.0),
        unselectedIconTheme: const IconThemeData(size: 26.0),
        selectedItemColor: BrandColor.primary[500],
        unselectedItemColor: BrandColor.primary[300],
      ),
    );
  }

  /// Styles for the dark theme.
  ThemeData darkThemeData() {
    return ThemeData.dark().copyWith(
      scaffoldBackgroundColor: BrandColor.secondary[700],
      primaryColor: BrandColor.primary[500],
      textTheme: GoogleFonts.interTextTheme(Theme.of(this).textTheme).copyWith(
        bodyText2: TextStyle(color: BrandColor.secondary[500]),
        subtitle1: TextStyle(color: BrandColor.secondary[500]),
        headline4: TextStyle(color: BrandColor.primary[500]),
      ),
      textButtonTheme: TextButtonThemeData(
        style: TextButton.styleFrom(
          primary: BrandColor.primary[500],
        ),
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          onPrimary: Colors.white,
          primary: BrandColor.primary[500],
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16.0),
          ),
          padding: const EdgeInsets.symmetric(vertical: 18.0),
        ),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: OutlinedButton.styleFrom(
          primary: BrandColor.primary[500],
          side: BorderSide(
            color: BrandColor.primary[500] as Color,
          ),
        ),
      ),
      bottomNavigationBarTheme: BottomNavigationBarThemeData(
        backgroundColor: BrandColor.secondary[900],
        showSelectedLabels: false,
        showUnselectedLabels: false,
        selectedIconTheme: const IconThemeData(size: 26.0),
        unselectedIconTheme: const IconThemeData(size: 26.0),
        selectedItemColor: BrandColor.primary[500],
        unselectedItemColor: BrandColor.primary[300],
      ),
    );
  }
}
```

Yay! Now we can rewrite widget `./lib/screens/app.dart` like this:

```dart
// ./lib/screens/app.dart

/// Application widget.
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      debugShowCheckedModeBanner: false,
      theme: context.lightThemeData(),    // 👈 default theme (light)
      darkTheme: context.darkThemeData(), // 👈 dark theme
      themeMode: ThemeMode.system,
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (_) => const SplashScreen(),
        '/sign-in': (_) => const SignInScreen(),
        '/main': (_) => const MainScreen(),
        // ...
      },
    );
  }
}
```

The app works and looks the same, but the development experience has grown several times over. What a great way to decompose code! 🎉

[↑ Table of contents](#toc)

<a name=""></a>
## Photos and videos by

- Filip Hracek & the Flutter team https://flutter.dev
- Vic Shóstak https://shostak.dev

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv