Set theme in Flutter, Best Practices in Material 3

How do we set the theme in Flutter 3.0? Moreover, what are the best practices to use tools and APIs introduced in Material 3

We’ve tried to make the steps as simple as possible so the beginners can also follow along.

Therefore, in this section we will learn to write a Flutter app that sets its theme once and looks beautiful across all platforms.

Furthermore we’ll also learn to design text so that it gives users a good experience.

Firstly, to share colors and font styles throughout an app, we use themes. 

As a result, we’ve defined app-wide themes and provide the custom theme to the child widgets. 

Secondly, we’ve defined the colors and font styles for a particular part of the application. 

To do that we have used a few useful Flutter packages.

Let’s add the dependencies in the “pubspec.yaml” file first.

dependencies:
  flutter:
    sdk: flutter  
  
  cupertino_icons: ^1.0.2
  provider: ^6.0.2
  material_color_utilities: ^0.1.4  
  dynamic_color: ^1.1.2
  google_fonts: ^2.3.1

Certainly there are two approaches that work in Flutter.

We can create custom widgets with its own theme. However, the better approach is to create a scoped theme for default widgets.

We’ve used the second approach.

Why?

Because once we create the app-wide theme at the root of a Flutter app, we can start using it everywhere.

As an outcome Flutter’s Material widgets also use our Theme to set the color for every widget.

How to set theme in Flutter

To create a consistent theme across the whole app we use the theme provider in our “model” folder.

import 'dart:math';

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

class ThemeSettingChange extends Notification {
  ThemeSettingChange({required this.settings});
  final ThemeSettings settings;
}

class ThemeProvider extends InheritedWidget {
  const ThemeProvider(
      {super.key,
      required this.settings,
      required this.lightDynamic,
      required this.darkDynamic,
      required super.child});

  final ValueNotifier<ThemeSettings> settings;
  final ColorScheme? lightDynamic;
  final ColorScheme? darkDynamic;

  Color custom(CustomColor custom) {
    if (custom.blend) {
      return blend(custom.color);
    } else {
      return custom.color;
    }
  }

  Color blend(Color targetColor) {
    return Color(
        Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
  }

  Color source(Color? target) {
    Color source = settings.value.sourceColor;
    if (target != null) {
      source = blend(target);
    }
    return source;
  }

  ColorScheme colors(Brightness brightness, Color? targetColor) {
    final dynamicPrimary = brightness == Brightness.light
        ? lightDynamic?.primary
        : darkDynamic?.primary;
    return ColorScheme.fromSeed(
      seedColor: dynamicPrimary ?? source(targetColor),
      brightness: brightness,
    );
  }

  ShapeBorder get shapeMedium => RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      );

  CardTheme cardTheme() {
    return CardTheme(
      elevation: 0,
      shape: shapeMedium,
      clipBehavior: Clip.antiAlias,
    );
  }

  ListTileThemeData listTileTheme(ColorScheme colors) {
    return ListTileThemeData(
      shape: shapeMedium,
      selectedColor: colors.secondary,
    );
  }

  AppBarTheme appBarTheme(ColorScheme colors) {
    return AppBarTheme(
      elevation: 0,
      backgroundColor: colors.surface,
      foregroundColor: colors.onSurface,
    );
  }

  TabBarTheme tabBarTheme(ColorScheme colors) {
    return TabBarTheme(
      labelColor: colors.secondary,
      unselectedLabelColor: colors.onSurfaceVariant,
      indicator: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            color: colors.secondary,
            width: 2,
          ),
        ),
      ),
    );
  }

  BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
    return BottomAppBarTheme(
      color: colors.surface,
      elevation: 0,
    );
  }

  BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
    return BottomNavigationBarThemeData(
      type: BottomNavigationBarType.fixed,
      backgroundColor: colors.surfaceVariant,
      selectedItemColor: colors.onSurface,
      unselectedItemColor: colors.onSurfaceVariant,
      elevation: 0,
      landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
    );
  }

  NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
    return const NavigationRailThemeData();
  }

  DrawerThemeData drawerTheme(ColorScheme colors) {
    return DrawerThemeData(
      backgroundColor: colors.surface,
    );
  }

  ThemeData light([Color? targetColor]) {
    final colorScheme = colors(Brightness.light, targetColor);
    return ThemeData.light().copyWith(
      //pageTransitionsTheme: pageTransitionsTheme,
      colorScheme: colorScheme,
      appBarTheme: appBarTheme(colorScheme),
      cardTheme: cardTheme(),
      listTileTheme: listTileTheme(colorScheme),
      bottomAppBarTheme: bottomAppBarTheme(colorScheme),
      bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
      navigationRailTheme: navigationRailTheme(colorScheme),
      tabBarTheme: tabBarTheme(colorScheme),
      drawerTheme: drawerTheme(colorScheme),
      scaffoldBackgroundColor: colorScheme.background,
      useMaterial3: true,
    );
  }

  ThemeData dark([Color? targetColor]) {
    final colorScheme = colors(Brightness.dark, targetColor);
    return ThemeData.dark().copyWith(
      colorScheme: colorScheme,
      appBarTheme: appBarTheme(colorScheme),
      cardTheme: cardTheme(),
      listTileTheme: listTileTheme(colorScheme),
      bottomAppBarTheme: bottomAppBarTheme(colorScheme),
      bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
      navigationRailTheme: navigationRailTheme(colorScheme),
      tabBarTheme: tabBarTheme(colorScheme),
      drawerTheme: drawerTheme(colorScheme),
      scaffoldBackgroundColor: colorScheme.background,
      useMaterial3: true,
    );
  }

  ThemeMode themeMode() {
    return settings.value.themeMode;
  }

  ThemeData theme(BuildContext context, [Color? targetColor]) {
    final brightness = MediaQuery.of(context).platformBrightness;
    return brightness == Brightness.light
        ? light(targetColor)
        : dark(targetColor);
  }

  static ThemeProvider of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
  }

  @override
  bool updateShouldNotify(covariant ThemeProvider oldWidget) {
    return oldWidget.settings != settings;
  }
}

class ThemeSettings {
  ThemeSettings({
    required this.sourceColor,
    required this.themeMode,
  });

  final Color sourceColor;
  final ThemeMode themeMode;
}

Color randomColor() {
  return Color(Random().nextInt(0xFFFFFFFF));
}

// Custom Colors
const linkColor = CustomColor(
  name: 'Link Color',
  color: Color(0xFF00B0FF),
);

class CustomColor {
  const CustomColor({
    required this.name,
    required this.color,
    this.blend = true,
  });

  final String name;
  final Color color;
  final bool blend;

  Color value(ThemeProvider provider) {
    return provider.custom(this);
  }
}

In this section, after setting the theme we’ve defined how to use the ThemeData widget.

Next, we have extended Notification class, and Inherited Widget.

The reason is to use the same workflow across the whole app. Not only that, we’ll also show how a child widget can use its own color.

Most importantly, the provider plays an important role. Because we’ve created an instance and passed it to the scoped theme at the root of the app.

import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'model/counting_the_number.dart';
import 'model/theme.dart';

final settings = ValueNotifier(
  ThemeSettings(
    sourceColor: Colors.pink,
    themeMode: ThemeMode.system,
  ),
);

void main() => runApp(
      ChangeNotifierProvider(
        create: (context) => CountingTheNumber(),
        builder: (context, _) => DynamicColorBuilder(
          builder: (lightDynamic, darkDynamic) => ThemeProvider(
            lightDynamic: lightDynamic,
            darkDynamic: darkDynamic,
            settings: settings,
            child: const FlutterMaterial(),
          ),
        ),
      ),
    );

As a result, now any nested child widget inherits this theme object. 

Let’s see what it looks like first.

Set theme in Flutter opening screen
Set theme in Flutter opening screen

We’ve used Default tab controller, Nested scroll view and TabBar View widgets to show separate tabs.

Clicking the tabs takes us to different screens. 

In addition we can click any page and see how it looks like.

Set theme in Flutter home page
Set theme in Flutter home page

We would like to see the code of the Home page. For one reason. We’ve used a particular color by accessing the color role on the color scheme.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Home Page',
          style: GoogleFonts.adventPro(
            fontSize: 30.0,
            fontWeight: FontWeight.bold,
            color: Theme.of(context).cardTheme.shadowColor,
          ),
        ),
      ),
      body: Center(
        child: Container(
          decoration: BoxDecoration(
            border: Border.all(
              color: Theme.of(context).colorScheme.outline,
              width: 10,
            ),
          ),
          child: Text(
            'Home Page',
            style: GoogleFonts.laila(
              fontSize: 30.0,
              fontWeight: FontWeight.bold,
              color: Theme.of(context).cardTheme.color,
            ),
          ),
        ),
      ),
    );
  }
}

What is the advantage of Material Design 3? 

The “color role” plays an important role. As an outcome we see that each “color role” compliments each other.

In the next section we’ll discuss it in detail and we’ll also try to apply throughout the User Interface.

By the way, although this Flutter app with Material 3 is not complete yet, still you can clone this branch of GitHub repository.

What Next?

Books at Leanpub

Books in Apress

My books at Amazon

GitHub repository

Python and Data Science

Twitter

Comments

One response to “Set theme in Flutter, Best Practices in Material 3”

  1. […] Firstly, it has many advantages, such as we can change the theme from dark to light or vice versa. […]

Leave a Reply