Theming a Flutter App: Getting Started

Learn how to make your app stand out by styling widgets, creating a dynamic theme, and toggling between available themes. By Andres Torres.

4.2 (12) · 2 Reviews

Download materials
Save for later
Share

One of the great features of Flutter is that it gives you absolute control of every pixel on the screen. This allows developers to implement the designer’s vision without compromise.

Nowadays, vanilla widgets look a bit dull. Customizing your widgets provides hierarchy, directionality and structure to the whole UI — helping with user acquisition and engagement.

In this tutorial, you’ll apply styles to widgets, centralize your app’s theme and dynamically toggle between different themes.

In the process, you’ll learn how to:

  • Style specific widgets.
  • Use an overall theme for your app.
  • Toggle between light and dark themes.

Now, it’s time to embellish your project with beautiful themes!

Note: This tutorial assumes you’re familiar with the basics of Flutter. If you’re new to Flutter, check out this Flutter tutorial and this more in-depth Flutter UI Widgets tutorial.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Then, open the starter project in Android Studio, where you’ll find Knight and Day, an activity-logging app for knights on duty.

Build and run. You’ll see the following screen:

Vanilla widget styles

On the Home screen, you’ll see a count of different activities you, as a knight, have logged. Pressing a button corresponding to one of the activities increments the activity count.

Even though the app works, it looks bland. You’ll use different methods to give the widgets some personality, making your app stand out from all the others in the ecosystem. :]

Before you start coding, take a moment to review some background about styling and theming and why they’re important.

What Is Theming?

Theming is the process of using a set of colors, fonts, shapes and design styles throughout your app. It’s a way to centralize all your stylistic decisions in one place.

Since most of your widgets share similar styles, you don’t want to style widgets individually. Instead, you should define the styles in one place and then reuse them. This will keep your codebase DRY (Don’t Repeat Yourself), readable and easy to maintain.

These are best practices to keep in mind when developing your app. If this doesn’t make sense right now, don’t worry! You’ll theme the app from the ground up and quickly realize the benefits of organizing your styles this way.

Theming in Flutter

Most visual widgets in Flutter have a style property whose type varies depending on the widget. Examples include TextStyle for the Text widget or ButtonStyle for the Button widget. Specifying a style will only affect that specific widget.

The idiomatic approach to styling components in Flutter is to add Theme widgets to the widget hierarchy. The higher level Material and Cupertino libraries even provide their own baked in themes to match the design languages they are implementing.

The Theme widget automatically applies its style to all descendant widgets. It takes a ThemeData argument, which holds the actual definition for the colors and font styles. If you look into the Flutter source code, you’ll even notice that a Theme widget just uses an InheritedWidget under the hood to distribute the ThemeData throughout the widget tree.

Applying a theme in Flutter is pretty straightforward. You’ll see how to do it next!

Note: Descendant widgets can obtain the current theme’s ThemeData using Theme.of. This is useful for making one-off variations based on the inherited style using copyWith to overwrite attributes.

Styling Widgets

The first thing you’ll learn is how to style specific widgets independently from one another.

Open lib/home/home_page.dart and go to the build method. There are three plain RaisedButton widgets. Your first task is to add shape and color attributes to style the buttons.

To start, replace the build method with the following:

@override
Widget build(BuildContext context) {
  final totalActivities = _joustCounter + _breakCounter + _patrolCounter;
  return Scaffold(
    appBar: CustomAppBar(
      title: 'Knight and Day',
    ),
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        BarChart(
          joustCounter: _joustCounter,
          breakCounter: _breakCounter,
          patrolCounter: _patrolCounter,
        ),
        const SizedBox(
          height: 32.0,
        ),
        Text('You\'ve done $totalActivities activities in total'),
        const SizedBox(
          height: 32.0,
        ),
        RaisedButton(
          child: const Text('Joust'),
          onPressed: () => setState(() { _joustCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
        RaisedButton(
          child: const Text('Take break'),
          onPressed: () => setState(() { _breakCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
        RaisedButton(
          child: const Text('Patrol'),
          onPressed: () => setState(() { _patrolCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
      ],
    ),
  );
}

Now all of the RaisedButton widgets have a shape attributes to make them into rounded rectangles and they’ve been given a light purple color

Build and run and you’ll see your new, fancy buttons.

Styled buttons

Note: You’ve probably noticed that you’re using a class called CustomColors from lib/theme/colors.dart. Following the DRY principle, this is a class that simply holds static values for the different colors you’ll use in this tutorial. If you need to change a color for any reason, instead of going through your entire codebase and changing each individual value, you can open CustomColors and simply change it there.

Providing an Overall Theme

You’ve seen how easy it’s to adjust the styling of a couple of widgets. When the number of widgets you want to style grows, however, it can become cumbersome to update all of those widgets when you want to change your apps style. Ideally, you want to have a single place that defines styles for your whole app. Luckily, you can achieve this by setting a Theme for your MaterialApp!.

Create a new file under lib/theme, name it custom_theme.dart and add the following code to the empty file:

import 'package:flutter/material.dart';

import 'colors.dart';

class CustomTheme {
  static ThemeData get lightTheme { //1
    return ThemeData( //2
      primaryColor: CustomColors.purple,
      scaffoldBackgroundColor: Colors.white,
      fontFamily: 'Montserrat', //3
      buttonTheme: ButtonThemeData( // 4
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
        buttonColor: CustomColors.lightPurple,
      )
    );
  }
}

You probably already have the hang of most of the code above. To break it down:

  1. You’re providing a static getter that’s globally accessible. You’ll use it later on.
  2. You’re building your actual custom ThemeData here. Notice the number of attributes you’ll override — this is just a handful of the possibilities.
  3. You’re also defining the font family your text will take by default.
  4. Here, you define button styling, similar to what you did previously in lib/home/home_page.dart.

Now, the only thing left to do is apply the theme. Open the lib/main.dart file and replace the contents of the file with the following:

import 'package:flutter/material.dart';
import 'package:knight_and_day/home/home_page.dart';
import 'package:knight_and_day/theme/custom_theme.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Knight and Day',
      home: HomePage(),
      theme: CustomTheme.lightTheme,
    );
  }
}

The only thing that’s actually changed is you’re using the theme attribute of MaterialApp and supplying the custom theme you created in the previous step.

Build and run and you’ll see that the font family has changed globally.

Light theme

Now that you’ve setup a global theme for your apps colors, text, and button styles you’ll want to remove the one off styles you added to the RaisedButtons earlier. Revert the lib/home/home_page.dart build method back to its original code:

@override
Widget build(BuildContext context) {
  final totalActivities = _joustCounter + _breakCounter + _patrolCounter;
  return Scaffold(
    appBar: CustomAppBar(
      title: 'Knight and Day',
    ),
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        BarChart(
          joustCounter: _joustCounter,
          breakCounter: _breakCounter,
          patrolCounter: _patrolCounter,
        ),
        const SizedBox(
          height: 32.0,
        ),
        Text('You\'ve done $totalActivities activities in total'),
        const SizedBox(
          height: 32.0,
        ),
        RaisedButton(
          child: const Text('Joust'),
          onPressed: () => setState(() { _joustCounter++; }),
        ),
        RaisedButton(
          child: const Text('Take break'),
          onPressed: () => setState(() { _breakCounter++; }),
        ),
        RaisedButton(
          child: const Text('Patrol'),
          onPressed: () => setState(() { _patrolCounter++; }),
        ),
      ],
    ),
  );
}

Build and run again, and you’ll notice that the buttons are still styled even though you removed the styles from each RaisedButton. They’re now inheriting the button theme from the theme you added to MaterialApp earlier on.

Great job! You now have a solid foundation for theming your app.