Skip to main content

Command Palette

Search for a command to run...

Atomic and Low-Level Design in Flutter: Crafting Scalable UI Components

Updated
5 min read
Atomic and Low-Level Design in Flutter: Crafting Scalable UI Components

Introduction

Flutter enables us to create very expressive UI easily. But for building a quality app contains many different components like Design systems, file structure, naming conventions, and many low level system design concepts. This article will particularly be about how we created both simple yet efficient component library using atomic design methodology and low level system design principles.

Understanding Atomic Design Methodology

Atomic Design, introduced by Brad Frost, structures UI components into five levels:

  1. Atoms — Atoms are the fundamental UI elements (e.g., Buttons, Labels, Icons, Input Fields)

  2. Molecules — Groups of atoms forming a molecule (e.g., Search Bar, Labelled input fields)

  3. Organisms — Combinations of molecule forming distinct UI sections (e.g., Navbars)

  4. Templates — Layouts defining content structure (e.g., Row, 2 Column Layout, Grid)

  5. Pages — Fully assembled UI screen

Understanding Low Level System Design

  1. Encapsulation — Manages Its own state and behaviour

  2. Reusability — Reusable across entire application

  3. Scalability — Should support multiple variations and allow future extensions seamlessly

Implementing a Scalable Button Component

For this article, we will focus on creating the atomic button. Buttons are simple to experiment with, easy to explain, and serve as a fundamental component that effectively demonstrates the implementation.

Let’s start with creating design tokens

Assuming all theme properties are applied using Flutter’s theme engine, we create tokens for button sizes. Many design systems, such as Material and Human Interface Design, define standard button heights. In our project, we follow SMRL (Small, Medium, Regular, Large) conventions:

Create a mappings for this sizes:

static const Map<ButtonSize, double> _buttonHeights = {
    ButtonSize.small: 36.0,
    ButtonSize.medium: 44.0,
    ButtonSize.regular: 52.0,
    ButtonSize.large: 60.0,
  };

This static map ensures that if we ever need to add more height parameters we can just add parameters to Enum and static map. This makes the component scalable to new variations easy.

Defining the button style

Majorly the button style remains fix UI wide, and this is just our primary button. We are using 4 types of button in our design system, Primary, Secondary, Tertiary and Textual Button more about them later. Just using the flutter theme engine to simplify lots of complexity here:

ButtonStyle getButtonStyle(BuildContext context) {
    return FilledButton.styleFrom(
      minimumSize: Size.fromHeight(buttonHeight),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
      textStyle: Theme.of(context).textTheme.labelLarge,
    );
  }

Now the main part Button Widget

This consists of 4 parameters which user can define:

final VoidCallback? onPressed;
final String label;
final Widget? icon;
final ButtonSize size;

In out design system we are utilising 4 material buttons (Filled, Elevated, Textual, Outlined). In primary button we are using the filled button. So as per the variables defined lets look at the build method of the widget.

Widget build(BuildContext context) {
    return icon == null
        ? FilledButton(
            onPressed: onPressed,
            style: getButtonStyle(context),
            child: Text(label),
          )
        : FilledButton.icon(
            onPressed: onPressed,
            style: getButtonStyle(context),
            label: Text(label),
            icon: icon!,
          );
  }

Here as we can see that if icon is null than the normal FilledButton will be rendered else the FilledButton.icon widget will be rendered.

Also the style is being fetched from the styles we created earlier. As you can observe this makes the code of widget itself seemingly very light and seperate from any other clutter.

Final Component

Now lets look at the full code of the widget, As I mentioned earlier we are using 4 material button components, This is our Primary Button Widget.

import 'package:flutter/material.dart';

/// Defines button sizes
enum ButtonSize { small, regular, medium, large }

/// Defines a primary button with configurable size and icon
class PrimaryButton extends StatelessWidget {
  final VoidCallback? onPressed;
  final String label;
  final Widget? icon;
  final ButtonSize size;

  const PrimaryButton({
    Key? key,
    required this.onPressed,
    required this.label,
    this.icon,
    this.size = ButtonSize.regular,
  }) : super(key: key);

  /// Predefined button heights
  static const Map<ButtonSize, double> _buttonHeights = {
    ButtonSize.small: 36.0,
    ButtonSize.medium: 44.0,
    ButtonSize.regular: 52.0,
    ButtonSize.large: 60.0,
  };

  /// Retrieves button height
  double get buttonHeight => _buttonHeights[size] ?? 44.0;

  /// Retrieves button style
  ButtonStyle getButtonStyle(BuildContext context) {
    return FilledButton.styleFrom(
      minimumSize: Size.fromHeight(buttonHeight),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
      textStyle: Theme.of(context).textTheme.labelLarge,
    );
  }

  @override
  Widget build(BuildContext context) {
    return icon == null
        ? FilledButton(
          onPressed: onPressed,
          style: getButtonStyle(context),
          child: Text(label),
        )
        : FilledButton.icon(
          onPressed: onPressed,
          style: getButtonStyle(context),
          label: Text(label),
          icon: icon!,
        );
  }
}

This button is being used application wide. It follow the low level design and Solid principle concepts:

Encapsulation — both the height and style guide is abstracted and developer don't need to bother about it.

Reusable — It is used all over screens in the application. the height can be customised as per need.

Scalability — It is scalable as it has multiple height support, styles can be pretty much same UI wide, and the labels and onpressed activity is handled outside of the component that give developer flexibility to customise the click action as per his need.

It follows the atomic guideline quite well:

Atom: PrimaryButton is a fundamental UI component.

Reusable: Works across multiple UI sections and screens.

Final Note

Using this methods, we were able to create a pretty good component library of our design system which follows practices around the Low Level Design and Atomic Design methodology. This makes any new developer onboarding easier, Faster development as don't need to focus on minor details like sizes and properties, and quality of components greatly enhanced as it is already created and quality tested. This improves overall Quality of life of the developer.

Here, attaching some references which inspired and helped us to create this library:

Hawkins: Diving into the Reasoning Behind our Design System

by Hawkins team member Joshua Godi; with cover art from Martin Bekerman and additional imagery from Wiki Chaves

netflixtechblog.com

Wise Design

Wise Design is the Wise design system. It helps our team create a distinct, accessible and consistent Wise experience…

wise.design

Lightning Design System 2

Lightning Design System 2 · Design system documentation, made with zeroheight

www.lightningdesignsystem.com