Design System in Flutter. Yeah I am optimising the title space.

Design System in Flutter. Yeah I am optimising the title space.

One of the key principles of writing better code is DRY - DON'T REPEAT YOURSELF.

The rise of no-code solutions has been explosive and is a testament to the fact that any code written is just more work to do on maintenance and upgrades.

So what is a design system? It’s a style guide that helps you build consistent UI as you grow your app and add new functionality. You get detailed and simple design systems, in both of them, there are a few things that have to be defined to qualify as a design system (for us when we request designs from a designer).

  1. Text Styling used in the design - Font Family, font weights for title, title 2, title 3, heading, body, etc.

  2. Buttons - All button types to be used in the app

  3. Input Fields - Showing all states, with leading (if any) focuses (if any) disabled, etc.

In this tutorial, I will go through building some parts of this that we need for the UI that we currently have and want to build next. The only things we’ll cover is:

  • Text Styles

  • Main Button

  • Outline Button

  • Input Field

Creating a design system in Flutter offers several benefits for app development, especially when you are working on a project with multiple team members or aiming for a consistent and cohesive user experience. Here are some of the key benefits:

  1. Consistency: A design system helps maintain a consistent look and feel throughout your app. This consistency is crucial for user satisfaction and brand identity. With predefined styles, components, and guidelines, you can ensure that every part of your app adheres to the same design principles.

  2. Faster Development: Design systems provide reusable components and styling, which significantly speeds up the development process. Developers can use pre-built widgets and styles, reducing the need to create custom solutions for every screen or feature.

  3. Easier Maintenance: When you make updates or changes to your app's design, a design system makes it easier to propagate those changes across the entire application. You can update a single component or style in the design system, and those changes will automatically reflect in all relevant parts of the app.

  4. Streamlined Collaboration: Design systems promote collaboration between designers and developers. Designers can create UI components and guidelines that developers can easily implement. This reduces communication gaps and misunderstandings, leading to a smoother development process.

  5. Scalability: As your app grows and evolves, a design system allows you to scale your development efforts efficiently. New features can be built using existing design system components, ensuring they align with your app's overall design.

  6. Accessibility: Design systems often include accessibility guidelines and practices. This ensures that your app is accessible to a wider range of users, including those with disabilities, which is not only ethically responsible but also often legally required.

  7. Branding and Identity: Design systems help reinforce your app's brand identity by enforcing consistent branding elements such as color schemes, typography, and iconography. This strengthens brand recognition and trust among users.

  8. Rapid Prototyping: With a design system in place, you can quickly create prototypes and mockups that accurately represent the final product. This is valuable for user testing and stakeholder feedback.

  9. Reduced Errors: Design systems help reduce the chances of inconsistencies and errors in your app's user interface. By defining standardized components and guidelines, you minimize the risk of accidental design deviations.

  10. Community and Open Source: Flutter has a growing community of developers who contribute to open-source design systems and libraries. You can leverage these resources to save time and ensure best practices are followed in your app.

  11. Cross-Platform Compatibility: Flutter allows you to use the same design system for both iOS and Android, making it easier to maintain a consistent user experience across different platforms.

  12. Cost-Efficiency: While there's an initial investment of time and effort in creating a design system, it can lead to significant cost savings in the long run. Reusable components and streamlined development processes reduce development time and costs.

In summary, a design system in Flutter provides numerous advantages, including consistency, efficiency, collaboration, and scalability, all of which contribute to a better user experience and a more efficient development process. It's a valuable investment for any Flutter app project, especially those with long-term goals.

Package Setup

We’ll be building our design system ui in a separate package. This is not required. The reason we’re doing this is because it allows us to import code easily across different apps that might be using the same design language. This will make it easy for us to share all those widgets and styles later on.

We’ll start by creating a new package called jarvis (Cause I Love You 3000).

flutter create --template=package jarvis

When that’s complete we want to create the example app for the package. Navigate into the jarvis folder and create the example app.

cd jarvis
flutter create example

Open up the example project and then we’ll add a relative path to the box_ui package.

jarvis:
  path: ../

Code

In the box_ui lib folder we’ll create a new folder called src. This will contain all the package code that we don’t want to be visible outside of the package unless we expose it.

Colour

We’ll start by creating some shared "colors" based on the colours defined in the design system. We’ll create a file in lib/src/shared/colors.dart

We will create a separate class "CustomColors" to ensure we are using colours defined only here in our widgets and throughout the project using type definitions.

import 'package:flutter/material.dart';

class CustomColors {
  Brightness mode;

  Color primary10;
  Color primary20;
  Color primary30;
  Color primary40;
  Color primary50;
  Color primary60;
  Color primary70;
  Color primary80;
  Color primary90;

  Color secondary10;
  Color secondary20;
  Color secondary30;
  Color secondary40;
  Color secondary50;
  Color secondary60;
  Color secondary70;
  Color secondary80;
  Color secondary90;

  CustomColors({
    required this.mode,
    required this.primary10,
    required this.primary20,
    required this.primary30,
    required this.primary40,
    required this.primary50,
    required this.primary60,
    required this.primary70,
    required this.primary80,
    required this.primary90,
    required this.secondary10,
    required this.secondary20,
    required this.secondary30,
    required this.secondary40,
    required this.secondary50,
    required this.secondary60,
    required this.secondary70,
    required this.secondary80,
    required this.secondary90,
  });

  static final light = CustomColors(
    mode: Brightness.light,
    primary10: const Color.fromARGB(237, 174, 10, 1),
    primary20: const Color.fromARGB(237, 174, 20, 1),
    primary30: const Color.fromARGB(237, 174, 30, 1),
    primary40: const Color.fromARGB(237, 174, 40, 1),
    primary50: const Color.fromARGB(237, 174, 50, 1),
    primary60: const Color.fromARGB(237, 174, 60, 1),
    primary70: const Color.fromARGB(237, 174, 70, 1),
    primary80: const Color.fromARGB(237, 174, 80, 1),
    primary90: const Color.fromARGB(237, 174, 90, 1),
    secondary10: const Color.fromARGB(237, 200, 10, 1),
    secondary20: const Color.fromARGB(237, 200, 20, 1),
    secondary30: const Color.fromARGB(237, 200, 30, 1),
    secondary40: const Color.fromARGB(237, 200, 40, 1),
    secondary50: const Color.fromARGB(237, 200, 50, 1),
    secondary60: const Color.fromARGB(237, 200, 60, 1),
    secondary70: const Color.fromARGB(237, 200, 70, 1),
    secondary80: const Color.fromARGB(237, 200, 80, 1),
    secondary90: const Color.fromARGB(237, 200, 90, 1),
  );

  static final dark = CustomColors(
    mode: Brightness.dark,
    primary10: const Color.fromARGB(125, 144, 10, 1),
    primary20: const Color.fromARGB(125, 144, 20, 1),
    primary30: const Color.fromARGB(125, 144, 30, 1),
    primary40: const Color.fromARGB(125, 144, 40, 1),
    primary50: const Color.fromARGB(125, 144, 50, 1),
    primary60: const Color.fromARGB(125, 144, 60, 1),
    primary70: const Color.fromARGB(125, 144, 70, 1),
    primary80: const Color.fromARGB(125, 144, 80, 1),
    primary90: const Color.fromARGB(125, 144, 90, 1),
    secondary10: const Color.fromARGB(125, 130, 10, 1),
    secondary20: const Color.fromARGB(125, 130, 20, 1),
    secondary30: const Color.fromARGB(125, 130, 30, 1),
    secondary40: const Color.fromARGB(125, 130, 40, 1),
    secondary50: const Color.fromARGB(125, 130, 50, 1),
    secondary60: const Color.fromARGB(125, 130, 60, 1),
    secondary70: const Color.fromARGB(125, 130, 70, 1),
    secondary80: const Color.fromARGB(125, 130, 80, 1),
    secondary90: const Color.fromARGB(125, 130, 90, 1),
  );
}

Text

Let's define our text styles now.

We start with defining our abstract class of TextStyle and using it in the consolidator class which contains all our custom styles.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CustomTextStyle extends TextStyle {
  const CustomTextStyle._style(TextStyle style) : super();
}

abstract class CustomTextStyles {
  static const _parent = TextStyle(color: Colors.black87);

  static final headline1 = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 34,
      fontWeight: FontWeight.w400,
    ),
  );

  static final headline2 = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 28,
      fontWeight: FontWeight.w600,
    ),
  );

  static final headline3 = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 24,
      fontWeight: FontWeight.w600,
    ),
  );

  static final headingStyle = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 30,
      fontWeight: FontWeight.w700,
    ),
  );

  static final bodyStyle = CustomTextStyle._style(
    _parent.copyWith(fontSize: 16, fontWeight: FontWeight.w400),
  );

  static final subHeadingStyle = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 20,
      fontWeight: FontWeight.w400,
    ),
  );

  static final captionStyle = CustomTextStyle._style(
    _parent.copyWith(
      fontSize: 12,
      fontWeight: FontWeight.w400,
    ),
  );
}

Helpers

These are small widgets and functions that help us in layout design. Defining them separately helps with maintaining consistent layout patterns.

// Horizontal Spacing
import 'package:flutter/material.dart';

const Widget horizontalSpaceTiny = SizedBox(width: 5.0);
const Widget horizontalSpaceSmall = SizedBox(width: 10.0);
const Widget horizontalSpaceRegular = SizedBox(width: 18.0);
const Widget horizontalSpaceMedium = SizedBox(width: 25.0);
const Widget horizontalSpaceLarge = SizedBox(width: 50.0);

const Widget verticalSpaceTiny = SizedBox(height: 5.0);
const Widget verticalSpaceSmall = SizedBox(height: 10.0);
const Widget verticalSpaceRegular = SizedBox(height: 18.0);
const Widget verticalSpaceMedium = SizedBox(height: 25.0);
const Widget verticalSpaceLarge = SizedBox(height: 50.0);

// Screen Size helpers

double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHeight(BuildContext context) => MediaQuery.of(context).size.height;

double screenHeightPercentage(BuildContext context, {double percentage = 1}) =>
    screenHeight(context) * percentage;

double screenWidthPercentage(BuildContext context, {double percentage = 1}) =>
    screenWidth(context) * percentage;

Widgets

Now, we are going to see how we create widgets using our custom Styles and Colors.

Text

lib/widgets/text.dart

import 'package:flutter/material.dart';
import '../shared/helpers.dart';
import '../shared/styles.dart';

class InfoText extends StatelessWidget {
  final String title;
  final String info;
  final CustomTextStyle style;
  const InfoText({
    Key? key,
    required this.title,
    required this.info,
    required this.style,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var width = screenWidth(context);
    var height = screenHeight(context);
    return Padding(
      padding: EdgeInsets.fromLTRB(width * 0.06, height * 0.01, 0, 0),
      child: SizedBox(
        height: height * 0.03,
        child: FittedBox(
          child: RichText(
            text: TextSpan(
              text: "$title : ",
              style: style,
              children: [
                TextSpan(
                  text: info,
                  style: TextStyle(
                    fontSize: height * 0.02,
                    fontWeight: FontWeight.w400,
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Input Field

lib/widgets/inputField.dart

import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:jarvis/shared/styles.dart';
import '../shared/helpers.dart';

class CustomField extends StatefulWidget {
  final Function(String) setValue;
  final String? Function(String?)? validator;
  final TextInputType? keyboardType;
  final GlobalKey<FormState> formKey;
  final bool obscureText;
  final String? initialValue;
  final String? hintText;
  final int? maxLines;
  final List<TextInputFormatter>? textFormatters;
  final TextEditingController? controller;
  final CustomTextStyle hintStyle;
  final bool readOnly;

  const CustomField({
    Key? key,
    required this.setValue,
    required this.formKey,
    this.obscureText = false,
    this.keyboardType = TextInputType.visiblePassword,
    this.validator,
    this.initialValue,
    this.controller,
    this.readOnly = false,
    this.hintText,
    this.maxLines = 1,
    this.textFormatters,
    required this.hintStyle,
  }) : super(key: key);

  @override
  State<CustomField> createState() => _CustomFieldState();
}

class _CustomFieldState extends State<CustomField> {
  @override
  Widget build(BuildContext context) {
    var width = screenWidth(context);
    var height = screenHeight(context);

    return Padding(
      padding: EdgeInsets.symmetric(horizontal: width * 0.05),
      child: Form(
        key: widget.formKey,
        child: TextFormField(
          maxLines: widget.maxLines,
          readOnly: widget.readOnly,
          controller: widget.controller,
          initialValue: widget.initialValue,
          validator: widget.validator,
          keyboardType: widget.keyboardType,
          cursorColor: Colors.black,
          inputFormatters: widget.textFormatters,
          style: TextStyle(fontSize: height * 0.018),
          onChanged: (value) {
            widget.setValue(value);
          },
          decoration: InputDecoration(
            filled: true,
            fillColor: Platform.isIOS
                ? const Color.fromRGBO(235, 235, 235, 1)
                : Colors.white,
            border: Platform.isIOS
                ? const OutlineInputBorder(
                    borderSide: BorderSide.none,
                    borderRadius: BorderRadius.all(Radius.circular(10)))
                : const OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10))),
            hintText: widget.hintText,
            hintStyle: widget.hintStyle,
          ),
          obscureText: widget.obscureText,
        ),
      ),
    );
  }
}

Expose Library Classes

The last thing is to expose the things we want to expose from the jarvis package.

library jarvis;

// Widgets Export
export '../widgets/inputField.dart';
export '../widgets/listItem.dart';
export '../widgets/text.dart';

// Colors Export
export '../shared/colors.dart';

And that’s it for building the basics for the design system that we’ll need.

GitHub Repo - https://github.com/saurabhdhingra/jarvis