Why building graphical user interfaces is hard.
Why is it so hard to write and maintain UI code? One of the reasons is poor API design. Specifically poor balance of control vs effort over a set of use cases. Effort is the time it takes to implement a use case using an API. Control is how fine-grained the commands that we send to an API can be.
This issue of balance is not GUI-specific, itâs a generic problem in API design. This post is my attempt to understand the dynamics between control and effort using examples from frontend development.
The lost art of geometry
Hereâs a puzzle. Letâs say we have two rectangles. One is taller and wider than the other. We want to place and vertically center the smaller rectangle inside the larger one.
The height of the larger rectangle is h1
. The height of the smaller one is h2
. How do we solve for x
, where x
is the vertical offset of one rectangle against the other?
In case youâd like to take a shot at it, here are a few options:
1. x = h1 / 22. x = (h1 - h2) / 23. x = (h1 + h2) / 2
You see, itâs only one line of Mathâââthat old unsexy programming language of spreadsheets.
If we transfer this problem from the world of forms into the world of HTML documents, itâs still solvable. This time with CSSâââthat new cool styling language of browsers.
.container {display: flex;align-items: center;}
A slightly harder puzzle
Letâs make it a titbit harder. Now weâd like to tie the upper side of the smaller rectangle to a grid. The rest is the same. We are still centering the smaller box within a larger one, only this time rounding the value of x
to the closest grid row.
The height of a grid row is h3
.
Puzzle: center a rectangle within a grid
1. x = h3 * round((h1 - h2) / (2 * h3))2. x = h3 * (h1 - h2) / 23. x = h3 * floor((h1 + h2) / 2)
The solution is more complex this time, but itâs still Math and itâs still just one line. How would we do this with CSS? With iOS AutoLayout? Android Layout?
Sadly, thereâs no way.
Whoâs got the rhythm?
Those puzzles were inspired by real-world use cases for GUIs. The last one is a model of Vertical rhythm, a concept from typography.
Vertical rhythm
Why is it so hard to implement? After all, weâre just arranging pixels on a screen. Pixels are squares. Squares are geometric shapes that we should be able to manipulate with ease. But we canât.
A pixel is not a pixel
The problem is that a pixel on the screen is represented by something else in the system. Itâs a DOM element, a View, an Object. Itâs a thousand different things. What a pixel actually is is hidden from a developer.
Browserâs render pipeline
In case of a browser it leads to all sorts of inconveniences:
- We are limited to the use cases that browser vendors consider common;
- We canât write tests for layout or paint stages of rendering;
- Testing anything UI-related requires spinning off a browser. Itâs like going groceries shopping on a private jet.
The wrong tradeoff
At the beginning of the article I said that the core issue here is:
Poor balance of control vs effort over a set of use cases.
Letâs unpack that statement.
The set of use cases of an API is everything a dev might want to implement. Browser vendors give us abilities to make GUIs. Any interface we might want to build is included in the set of use cases. Itâs a large set.
Effort is the time it takes to implement a use case using an API. The more time it takes (for the person who already knows how the API works) the harder it is to use.
What I mean by âcontrolâ is how fine-grained the commands that we send to an API can be. In a graphics API, the levels of control could vary from changing properties of a component to changing properties of a shape to manipulating a pixel.
Amount of control we have is often determined by the level of abstraction weâre working with. The lower the abstraction the closer to hardware we are the more control we have.
Level of Abstraction vs Effort relationship for different kinds of use cases
It might seem that these two parameters, effort and control, are in direct conflict. The more control we have the more work we have to do. That is true, but itâs not all the truth.
As we move through the set of use cases the effort/control relationship changes. Showing one paragraph of text on a screen is easy to do with high-level browser APIs. Itâs more and more effortful as we use lower level abstractions. Imagine coding a shader that draws characters of a given font.đ±
Now letâs remember the example with the grid at the beginning of the article. Some use cases are plain impossible with high-level abstractions. Until API vendors consider a use case like that common enough, thereâs no way we can implement it. At the same time, itâs not hard to make it using a high-control low-level API like OpenGL.
Weâre slowly approaching the topic of API design. What are our options as a system designer when we face these kinds of tradeoffs? One obvious choice would be to prioritise one part of the equation over the other.
#UseThePlatform
Historically browser vendors prioritized minimizing effort over a set of common use cases. Browsers were decent at displaying documents with text and images on a desktop screen.
Browser API as a flat surface
Issues began when publishers and developers started asking for more. Supporting several platforms, different screen resolutions and densities. Building apps, games and other interactive media. It all required giving more access, more control to the makers.
Browser API as a cheese
That led us into the situation we are in today. Thereâs that flat API for common cases with a bunch of âholesâ drilled into it to get to the lower-level functionality browsers have (and had for a while).
Thatâs why we can draw arbitrary shapes on an HTML canvas, but we canât make those shapes a proper part of DOM, CSSOM or AOM (Accessibility Object Model).
We can use different layout models like flexbox or grid. But thereâs no way we can unit-test the results of a layout stage.
We can to some degree control what a browser stores in its cache, but we can not save, retrieve and manipulate file contents in our code. At some point, we will be able to do that thanks to the File API. But it would be a separate construct, on a side, not related to Cache, Service Workers, Application Cache and other APIs. Even though all of those have the same foundation lower down the stack.
Browser API vs Browser insides
The hard way
Prioritizing either effort or control is one way to solve this conflict. Thereâs another way. The much harder way.
As designers of an API we can decide to expose it in layers. To give access to both low-level and high-level primitives. The tricky part is to do it in a way where a developer can mess around with a lower-level API without the result being âexcludedâ from the rest of the system.
Flutter is a good example of that kind of approach. Flutter is a cross-platform mobile development platform made by Google.
Flutter architecture
Because of the layered structure of the Flutter API we are able to:
- Make a widget that is responsible for laying out its children;
- Unit-test our custom layout widget without starting an emulator;
- Make a widget with a custom paint method. For example, we can make a button with a custom shape, interesting shadow or coloring effect;
- That custom widget will still remain a button in terms of gestures it accepts, accessibility and other properties;
- We could unit-test the paint operation too. Again, just by running the code in a Dart VM, no emulator required.
Going back to our grid example from the top of the article. Hereâs the code of the widget that centers its children vertically, tying them to a grid:
Offset getPositionForChild(Size containerSize,Size childSize) {final h1 = containerSize.height;final h2 = childSize.height;final h3 = rowHeight;final rowsCount = ((h1 - h2) / (2 * h3)).round();return Offset(0, rowsCount * h3);}
As you can see, the code in Flutter looks very similar to what we came up with using Math: x = h3 * round((h1 - h2) / (2 * h3)).
Thatâs another benefit of working on the right level of abstraction: the way we express our intention is straight-forward.
Hereâs how the resulting app in Flutter looks like:
Closing thoughts
These principles of effort-vs-conflict, layered-vs-flat helped me solve some of the recurring issues in my work. Itâs a useful lens to look through.
Sometimes we get caught up âdrillingâ our flat high-level API to extract the functionality that is burried underneath. Thatâs the sign that we might need to change our approach. Remember thereâs a much larger set of use cases that we might be able to support with a lower-level API. Extracting each use case one by one leads to bloated design and more work from you as an API designer.
This post was originally published on my blog.
If youâd like to learn more about different approaches to GUI development, check out this Code Podcast episode.
This article is based on an API Design talk I made. You can find the video of the talk here.