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:
- The same colors being repeated and referenced independently in separate files and classes, i.e., creating spaghetti threads.
- Themes in a module being partially tied to another one, i.e., tangling the spaghetti threads with no clear or systematic formula for creating the ties, and that's where things get messy.
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:
- Using Classes and OOP to Organize your Theme Code
- DO: Avoid Hungarian Notation (HN)
-
Centralizing your Theme with
ThemeDatain Flutter - Going Further: Managing Web and Mobile Themes with Media Queries
- 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:
- Extract all of the colors, shapes and sizes you use throughout the app.
-
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
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.
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.
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:- A very nice tutorial — whose code I'm gonna be partially using below — can be found in the Flutter official documentation: Use themes to share colors and font styles
-
Material Theming with Flutter
from Flutter Interact '19
- There are also plenty of online material theming tools which can spit out design dimensions an palettes for you.
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:
MediaQuery.of(context).size.heightMediaQuery.of(context).size.widthMediaQuery.of(context).textScaleFactor
If you wish to use
MediaQuery(ies) optimally in Flutter for responsive designs, do try to use it in conjunction with theLayoutBuilderandSafeAreawidgets. 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.