Android Test-Driven Development by Tutorials,
Second Edition – Now Updated!

Build testable, sustainable Android apps via JUnit, Mockito, and Espresso
by diving into test-driven development (TDD) in this newly-updated book.

Home Flutter Tutorials

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.

4.3/5 7 Ratings

Version

  • Dart 2.10, Flutter 1.22, Android Studio 4.2

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.

Theming Text Widgets

Text widgets are special because ThemeData gives you multiple text styles to choose from. To see this in action, change the text widget that reads “You’ve done x activities in total” in lib/home/home_page.dart to the following:

Text(
  'You\'ve done $totalActivities activities in total',
  style: Theme.of(context).textTheme.headline6,
),

Build and run, and you now have a nice big heading to look at. :]

Text Style

Feel free to explore the different text styles contained in the TextTheme object. You’ll see text styles broken down into headline, subtitle, and body groupings. You can use these groupings to apply consistent text styles throughout your applications. This can help you create a more consistent looking application with very little effort.

Creating a Dark Theme

Nowadays, most operating systems and apps have a dark theme to use in low-light environments or to simply to give a stylistic twist to your app. You want to add this great addition to the user experience to your app as well.

To do this, open lib/theme/custom_theme.dart and add the following below lightTheme:

static ThemeData get darkTheme {
  return ThemeData(
    primaryColor: CustomColors.darkGrey,
    scaffoldBackgroundColor: Colors.black,
    fontFamily: 'Montserrat',
    textTheme: ThemeData.dark().textTheme,
    buttonTheme: ButtonThemeData(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
      buttonColor: CustomColors.lightPurple,
    )
  );
}

This block may look complex, but it’s basically the same structure that you used for lightTheme. The difference is that the colors and styles you’re using make more sense as a dark theme.

To see this in action, open lib/main.dart and replace the theme attribute with your new dark theme:

return MaterialApp(
  title: 'Knight and Day',
  home: HomePage(),
  theme: CustomTheme.darkTheme,
);

Here, you’re simply passing CustomTheme.darkTheme instead of CustomTheme.lightTheme.

Build and run your app to see how your UI gets a beautiful revamp! :]

Dark theme

This is a great upgrade, but wouldn’t it be better if the user could change the theme by clicking a button? You’ll tackle that in the next section.

Toggling Between Themes

To toggle between themes, you need to keep track of the current theme. You’ll do this by creating a global instance of your theme and applying it whenever you need it. Start by creating config.dart in lib/theme and adding the following:

import 'package:knight_and_day/theme/custom_theme.dart';

CustomTheme currentTheme = CustomTheme();

This will be the instance of the custom theme you’ll use throughout the app.

Next, open lib/theme/custom_theme.dart and add the following code to the top of CustomTheme:

static bool _isDarkTheme = true;
ThemeMode get currentTheme => _isDarkTheme ? ThemeMode.dark : ThemeMode.light;

void toggleTheme() {
  _isDarkTheme = !_isDarkTheme;
  notifyListeners();
}

Two main things are happening here. First, you declared a private variable that tracks whether the current theme is dark and you map _isDarkTheme to its corresponding ThemeMode.

Second, you create a method that toggles _isDarkTheme and then notifies anyone who’s listening. To be able to call notifyListeners, you need to implement ChangeNotifier. Do this by changing CustomTheme‘s signature to the following:

class CustomTheme with ChangeNotifier {
...
}

Now, open lib/main.dart and replace its contents with the following code. Remember to import lib/theme/config.dart:

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

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

class MyApp extends StatefulWidget {
  const MyApp({Key key}): super(key: key);
  //1
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  @override
  void initState() {
    super.initState();
    currentTheme.addListener(() {
      //2
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Knight and Day',
      home: HomePage(),
      theme: CustomTheme.lightTheme, //3
      darkTheme: CustomTheme.darkTheme, //4
      themeMode: currentTheme.currentTheme, //5
    );
  }
}

Here’s a breakdown of what’s happening:

  1. You convert MyApp from a stateless to a stateful widget.
  2. Since you need ChangeNotifier to let you know when the theme changes, you subscribe to it in initState and listen to changes from currentTheme. You need to trigger a widget rebuild, so you call setState with nothing particular inside the closure.
  3. The default theme.
  4. The dark theme.
  5. Based on this attribute, MaterialApp knows which theme to apply: either theme or darkTheme.

The last step to make all this work is to toggle the theme by pressing the action button on the app bar. Open lib/custom_app_bar.dart and replace IconButton with the following:

IconButton(
  icon: const Icon(Icons.brightness_4),
  onPressed: () => currentTheme.toggleTheme(),
)

Here you added onPressed, a function that toggles between the different themes.

Remember to import lib/theme/config.dart:

import 'package:knight_and_day/theme/config.dart';

Now, you can build and run. Press the action button on the app bar and you’ll see how your theme changes dynamically. Great job!

Dynamic Theme

Where to Go From Here?

You can download the final version of this project using the Download Materials button at the top or bottom of this tutorial.

Congratulations on making your app shine with these beautiful themes! Now, you can leverage the theming system in Flutter to elevate your apps to new levels. Great design and user experience are key when trying to engage with users.

If you want to dive a bit deeper into the world of Flutter, check out Your First Flutter App. Another great resource is our awesome book, Flutter Apprentice.

If you have any questions, comments or want to show off great theming options for your app, feel free to join the discussion below!

Average Rating

4.3/5

Add a rating for this content

7 ratings

More like this

Contributors

Comments