Material You Flutter, Next Generation Design

When we want to build beautiful, scalable Flutter apps across platforms, the Material You Design is the best choice. 

The good news is Flutter 3.0 has good support for using Material You which is the next generation design.

We have been discussing Material You or Material Design 3 for Flutter in the last couple of sections. So you may have a look at them.

For example, we have discussed how we can use the Value Notifier, Notification class, and the new Navigation Bar to notify, or navigate to the child widgets about the Material You design.

In addition, we have shown how we can shift from dark to light themes and vice versa.

Let us try to make things simple as Material You has made the Material design simpler than before.

Firstly, let us take a look at the dark theme first.

Material You and Flutter 3.0
Material You and Flutter 3.0

Firstly, at the top we have used a custom Appbar. We’ll see the code in a minute.

Secondly, in between we have used the dark color role as the body background.

Finally, the Navigation Bar is the new addition to Flutter 3 as the Material You widget.

Certainly, we can change to the light theme, and the Material You will handle the transformation. 

As a result, it will change the background making it light, and in contrast it will change the Text color, Navigation Bar selected color, etc.

Let us see how the same Flutter app looks like in light color theme.

Navigation Bar in Flutter with Material 3
Navigation Bar in Flutter with Material 3

Material You, an adaptable design and Flutter

The new Material You is an adaptable design system. 

We will never claim that it was not in the past. On the contrary, from the very beginning, Material Design language has synced well with Flutter.

As the Material You is open source, developers can easily follow the design guidelines and modify that to fit their own Flutter apps.

As a result, across the whole Flutter app we have a consistent color scheme that optimizes the user’s experience.

In addition, Material You gives us a consistent performance across all platforms. Whether it’s Android, iOS, Web or Desktop.

To start with Material You in Flutter, we need a few packages 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

After that, we need to define the custom theme which extends Notification class.

Why?

Because we need to notify each child widget so that they can adapt the design guidelines that we are following. Right?

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);
  }
}

As Flutter 3 supports Material You, we can easily sync them. As an outcome what do we get?

We get a few Material You widgets, buttons, as well as typography.

However, we need to provide the guidelines at the very beginning of the Flutter 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.red,
    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(),
          ),
        ),
      ),
    );

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

  static const String _title = 'Flutter 3.0 Notification & Material 3';

  @override
  Widget build(BuildContext context) {
    final theme = ThemeProvider.of(context);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme.dark(settings.value.sourceColor),
// We can change it to light as follows
// theme: theme.light(settings.value.sourceColor),
      darkTheme: theme.dark(settings.value.sourceColor), 
      themeMode: theme.themeMode(),
      title: _title,
      home: const MyScaffold(),
    );
  }
}

As a whole, we have more control over the Flutter App, as we don’t have to think about the color contrast and other theme settings. 

Moreover, Material You refers to a set of colors that will define an interface. On the other hand, it also optimizes the user’s experience.

As a result, Material You defines colors as primary and secondary. The color slots for background, surface, errors or more match with each other.

All we need to do is to define the source color, and the theme data – light or dark.

Based on our choice, Material You theme manages everything in Flutter.

How Material You controls theme in Flutter

Material You gives us an overall control on every type of Material Widgets.

For example, think of color contrast. Since the background is dark, we don’t have to fine tune the color of text on every surface. 

To summarize, the change takes place implicitly. We don’t have to define color. Especially when the color needs the contrast requirements. 

Not only that, the text colors have different emphasis when the ThemeData property is dark.

There are more.

Material You design principles follow a guideline that has an intended meaning for each text style.

And it varies from big headlines to button text in a synchronized way. 

However, still we can change the color scheme using the color role as we want to change.

For example, consider the following code.

return Container(
      height: 116.0,
      decoration: BoxDecoration(
        border: Border.all(
          color: Theme.of(context).colorScheme.outline,
          width: 10,
        ),
      ),

In the above code we have applied a box decoration to the Container widget where we wanted the border with a certain color scheme.

The Material You design principles have managed the color. While doing so it keeps in mind how it should like when the ThemeData is either dark, or light.

As we progress, we will see more examples on Material You and Flutter.

So stay tuned.

What Next?

Books at Leanpub

Books in Apress

My books at Amazon

Courses at Educative

GitHub repository

Python and Data Science

Twitter

Comments

Leave a Reply