Avoiding Spaghetti Theming | With an Example in Flutter

Spaghetti Code is when you have references and dependencies all over the place, in multiple "threads", making it difficult for the reader to know where things start or end. They were notorious when people overused GOTO statements — and the biggest cause for the extinction of that statement.

Similarly, what I'm calling Spaghetti Theming is the counterpart in UI, that is, when UI elements are jumbled throughout the code in multiple different files, with no clear end nor beginning.

It's very easy, if you're not disciplined and methodical, to have theme dependencies go awry in that fashion. For example, by having:

Coming to think about it, this problem seems pretty common, doesn't it? And it's not that easy to think of a trivial solution... so how do we avoid this mess? The short answer is centralizing all of the references, but the longer answer would take into account all of the different programming techniques we can use to achieve this centralization:

  1. Using Classes and OOP to Organize your Theme Code
  2. DO: Avoid Hungarian Notation (HN)
  3. Centralizing your Theme with ThemeData in Flutter
  4. Going Further: Managing Web and Mobile Themes with Media Queries
  5. On the Designer's Responsibility

One of the most basic ways of avoiding this cross-referencing hell is to better organize your code and, if you're using Object-Oriented Programming (OOP), you can do so by putting the different elements of your theme in different classes. You will need to, then:

  1. Extract all of the colors, shapes and sizes you use throughout the app.
  2. Create classes for each of these thematic abstractions. For example, you could create a class named Colors, which would, evidently, hold all of the apps colors — that's basically creating your app's color palette.
    • Depending on the language you're using, you could even use interfaces or abstract classes since most of these theme classes will be simple static constants repositories.

With respect specially to the colors, I have to add that you might have to be even more disciplined than usual. If you're not careful, it's even easier than normal to get them to appear all over the place.

Take, as an example, the color white. Say you're using it as a background for many different types of containers and, every time, you're specifying something like Colors.white. Now, imagine you have to change this standard container background to dark blue, how would you do that easily?

In that case, you should create a color class, probably for each widget, with a specific variable for that background. But I will go even further: what if — and this is not that uncommon — you want to change your global, standard white to, say, a white-grey after you have created color classes for many different widgets?

Since you're always referencing the vanilla code for white in your separate widget color classes, you will have to change your white manually again for all of these different classes, etc. In this case, what you could do is create a variable that shields, abstracts the vanilla white, creating your own white in your own, custom color palette.

I think it's going to be clearer with the code example below:

          abstract class AppPalette {
    static const Color white = Colors.white;
  }
  
  abstract class Container {
    static final Color background = AppPalette.white;
  }
        

This technique is very common in graphic design where we create custom palettes for our current design and reference every subsequent color to this palette and not the base, vanilla colors, and I can only see benefits if we use it programmatically in code.

For more details on this topic, do check section 4 of this article for more concrete details on how to achieve what I suggested above.

The Hungarian Notation (HN) occurs when you prefix or suffix a variable's name with information about its characteristics or type. For example, if you have a set of variables you would like the reader to know are constants, it was usual — many people still use this technique, since it's quite intuitive — to prefix them with the letter k. For example, kLength could be a length constant in your program.

That sounds like a great idea for encoding more semantics into the variables' names, right? Well...

HN was very useful in the past when types and classes didn't exist because it was basically the only way of encoding semantic information for the reader. One of its weaknesses, though, is that it isn't programmatically used, meaning that someone could just as well create a non-constant variable prefixed by k and nobody could easily, programmatically know if that would indeed be a constant or not.

On the other hand, if you put, say kLength into a class whose job is to store constants, everyone will know what you're talking about — indented text is also easier to read than text stitched together — and, if you put non-constants inside it, your language's analyzer or compiler itself can programmatically give you feedback, warning you that something is off.

It follows, then, that, if you put kLength inside, say, a class named Constants, you will be redundant and make the code harder, more time-consuming to read. In the end, the most programmatic and easier-to-read way of accessing this constant would be — assuming it's a static constant — Constants.length.

Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged — the compiler knows the types anyway and can check those, and it only confuses the programmer.

Linus Torvalds, Linux Kernel Coding Style, Linux Kernel Documentation.

Besides what I discussed in the second section of this article about placing different aspects of UI in different classes, in Flutter, you can leverage the power of an overarching theme for your app which will serve as the default styling for your widgets.

Some additional resources for this topic if you would like to know more:

In Flutter, when configuring your MaterialApp, you can simply pass your custom theme through the theme property:

        MaterialApp(
  title: title,
  theme: ThemeData(
    brightness: Brightness.dark,
    fontFamily: 'Georgia',
  ),
)
      

As I discussed in section 2, you can also manage this ThemeData separately, in a Theming class for example:

        abstract class Theming {
static final ThemeData app = ThemeData(
  iconTheme: defaultIcon,
);

static const IconThemeData icon = IconThemeData(
  color: BasicColors.grey,
);
}
      

Note that I'm not using a suffix for icon, which might seem weird at first, but, if you remember that everything in this class is of type ThemeData, it feels more intuitive. These properties will all be accessed as Theming.someTheme later on, so yet another reason for why this will be manageable.

If you wish to override the theme for an individual widget, you can wrap it with the Theme widget and pass to this new widget your new custom ThemeData:

        Theme(
  data: ThemeData(
    accentColor: Colors.yellow,
  ),
  child: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
)
      

You can also override specific characteristics of the context's or app's theme by using the copyWith method:

        Theme(
  // Could also be `Theming.someTheme.copyWith(...)`:
  data: Theme.of(context).copyWith(accentColor: Colors.yellow),
  child: FloatingActionButton(
    onPressed: null,
    child: Icon(Icons.add),
  ),
);
      

Theming shouldn't be much harder than that, i.e., having an overarching custom theme with some overrides here and there. Hence, if you're having trouble trying to manage your theme, you're probably doing something wrong.

Managing multiple screen sizes can be time consuming and a maintenance nightmare, even with good practices. There is no panacea for this problem yet. However, Flutter uses a similar mechanism to the well-established practice of CSS Media Queries.

Inevitably, you're gonna have to change the layout of your screen depending on the width and height of your screen. And, for that, use the switch or if-else statements to return different sets of widgets to your screen. In order to extract the dimension over which you're basing the switch, you can use the MediaQuery built-in class in conjunction with the context. For example, you can access the height, width and accessibility factor of the user's device through:

If you wish to use MediaQuery(ies) optimally in Flutter for responsive designs, do try to use it in conjunction with the LayoutBuilder and SafeArea widgets. For more info, take a look at this tutorial, by Reso Coder.

As an extra, if you feel too annoyed by the accessibility factor because it's really tough to create a design that's bullet-proof against any font-size, you can simply disable it by harding coding a factor yourself. For example:

          MaterialApp(
  builder: (BuildContext context, Widget child) {
    final MediaQueryData data = MediaQuery.of(context);
    return MediaQuery(
      data: data.copyWith(
        textScaleFactor: 1.0, // hard-coding the accessibility factor
      ),
      child: child,
    );
  },
);
      

Hard-coding the accessibility factor isn't a UI/UX good practice, but it's understandable, at least during development.

For more on responsive design, do check the Flutter official documentation on the useful widgets for the task.

One of the sources of Spaghetti Theming has virtually nothing to do with code: design purely as art. Designers usually treat themselves as artists and that can burden the practicality of the project quite considerably if treated bluntly.

This artist mentality originates from putting the design itself as the end product of the whole project, which is the brainwashing you inevitably get from schools. However, the project's end product is the the app, not only the design, which, thus, puts the design in a position of serving the rest of the project and not the other way around.

What do I mean by all that?

The designer should produce a design that is art — no, we are not ditching that — constrained by the practicality of the real world.

For example, it's very easy to want a confimation button on a screen to be blue and, on another screen, to be blue-grey, because you, as a designer, thought it prettier. The difference is minor but you, as an artist, will notice and cherish it. The difference is minor but the programmers, as implementers, will also notice it and, yet, hate it. Why? It's another variable for them to manage, another dependency with constant maintenance cost and very low benefit.

If you keep making these minor tweaks, do you see how the app won't have a scalable, manageable theme and design? It's your responsibility, as a pragmatic designer, to keep the design beautiful and, yet, as simple as possible to maintain: it's art, but it's not a painting you can hang on the wall, with virtually zero maintenance.