Material 3 : Flutter Firebase, Provider Blog App Final

Material 3, which is a short form of Material Design 3, is the next generation theme for Flutter apps.

According to the Google team, it is the most expressive and adaptable design system yet.

How will you adopt Material 3, and apply a custom theme across your Flutter app, depends on you.

In other words, the Flutter 3.0 theme color we’ll take a new style using Material design 3. Certainly we will adopt the same principle in our ongoing Firebase, Provider Flutter app.

However, we need to understand how we can configure the overall visual Theme for a MaterialApp or a widget subtree within the app.

We’ve been building a Firebase, Firestore and Provider based web app where we have already used Material design 3.

Above all, we have reached the final stage. 

Let us explain how we have progressed step by step.

Firstly, we have added the dependencies to the “pubspec.yaml” file.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  firebase_core: ^1.14.0
  firebase_auth: ^3.3.13
  cloud_firestore: ^3.1.11
  google_fonts: ^2.3.1
  provider: ^6.0.2
  material_color_utilities: ^0.1.4  
  dynamic_color: ^1.1.2

The second step involves the most important step. Providing the values to the Firebase options constructor.

The API key, App ID, project ID, etc.

Since our Flutter app is cross-platform, we can provide multiple values as we have defined in our class.

import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
    show defaultTargetPlatform, kIsWeb, TargetPlatform;

/// we need to specify the associated values according to the platform
/// we're using, like in this case, we have chosen web platform
/// in Firebase console
///
class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      return web;
    }
    // ignore: missing_enum_constant_in_switch
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return android;
      case TargetPlatform.iOS:
        return ios;
      case TargetPlatform.macOS:
        return macos;
    }

    throw UnsupportedError(
      'DefaultFirebaseOptions are not supported for this platform.',
    );
  }

  static const FirebaseOptions web = FirebaseOptions(
    apiKey: "****************************************",
    appId: "*******************************************",
    messagingSenderId: "***********",
    projectId: "*********",
  );

  static const FirebaseOptions android = FirebaseOptions(
    apiKey: "****************************************",
    appId: "*******************************************",
    messagingSenderId: "***********",
    projectId: "*********",
  );

  static const FirebaseOptions ios = FirebaseOptions(
    apiKey: '',
    appId: '',
    messagingSenderId: '',
    projectId: '',
  );

  static const FirebaseOptions macos = FirebaseOptions(
    apiKey: '',
    appId: '',
    messagingSenderId: '',
    projectId: '',
  );
}

Next, we’ll use Material 3. Therefore, we will write a custom theme provider class that extends Inherited widget.

Why have we used the Inherited Widget?

Because we can apply the same theme across the whole widget tree. Right?

As a result, we have kept the custom theme class 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);
  }
}

As we have set two settings – light and dark, we can use any one of them.

In the beginning, we have seen how we can use the light theme.

Flutter 3.0 Material 3 Theme
Flutter 3.0 Material 3 Theme

But we can easily change the theme to the dark mode in our MaterialApp.

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

//import 'package:google_fonts/google_fonts.dart';

import '../main.dart';
import 'chat_home_page.dart';
import '../model/theme.dart';

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

  @override
  Widget build(BuildContext context) {
    final theme = ThemeProvider.of(context);
    return MaterialApp(
      title: 'Provider Firebase Blog',
      debugShowCheckedModeBanner: false,
      theme: theme.dark(settings.value.sourceColor),
      home: const ChatHomePage(),
    );
  }
}
Flutter Material 3 Web App
Flutter Material 3 Web App

Material 3 and Flutter 3.0

Flutter supports Material design from the very beginning. However, Material 3 is the next level design. As a result, Flutter also launches the 3.0 version which is much faster than the previous versions. 

Most importantly, it adopts Material Design 3.

Therefore we have successfully adopted Material 3. Now, we would like to give it a final touch.

 Previously we have seen that once the user posts a blog, it shows on the same page.

We’ve changed that part.

For example, we have changed the code a little bit. We’re no longer showing the blog titles on the same page. 

On the contrary, we have used a Text Button and navigated to another page.

import 'dart:async';

import 'package:blog_web_app_with_firebase/view/all_blogs.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import '../controller/all_widgets.dart';
import '../controller/authenticate_to_firebase.dart';
import '../model/state_of_application.dart';

class LetUsChatMessage {
  LetUsChatMessage({
    required this.name,
    required this.title,
    required this.body,
  });
  final String name;
  final String title;
  final String body;
}

class LetUsChat extends StatefulWidget {
  const LetUsChat({
    required this.addMessageOne,
    required this.messages,
  });
  final FutureOr<void> Function(String messageOne, String messageTwo)
      addMessageOne;
  final List<LetUsChatMessage> messages;

  @override
  State<LetUsChat> createState() => _LetUsChatState();
}

class _LetUsChatState extends State<LetUsChat> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_LetUsBlog');
  final _controllerOne = TextEditingController();
  final _controllerTwo = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Provider Firebase Blog',
          style: TextStyle(
            color: Theme.of(context).appBarTheme.foregroundColor,
          ),
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TextFormField(
                controller: _controllerOne,
                decoration: InputDecoration(
                  hintText: 'Title',
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(5),
                    borderSide: BorderSide(
                      color: Theme.of(context).highlightColor,
                      width: 1.0,
                    ),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(30),
                    borderSide: const BorderSide(
                      color: Colors.purple,
                      width: 2.0,
                    ),
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
              Expanded(
                child: SizedBox(
                  height: 150.0,
                  child: TextFormField(
                    controller: _controllerTwo,
                    maxLines: 10,
                    decoration: InputDecoration(
                      hintText: 'Body',
                      enabledBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(5),
                        borderSide: BorderSide(
                          color: Theme.of(context).highlightColor,
                          width: 1.0,
                        ),
                      ),
                      focusedBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(30),
                        borderSide: const BorderSide(
                          color: Colors.purple,
                          width: 2.0,
                        ),
                      ),
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
              ),
              const SizedBox(width: 10.0),
              StyledButton(
                onPressed: () async {
                  if (_formKey.currentState!.validate()) {
                    await widget.addMessageOne(
                        _controllerOne.text, _controllerTwo.text);
                    _controllerOne.clear();
                    _controllerTwo.clear();
                  }
                },
                child: Row(
                  children: const [
                    Icon(Icons.send),
                    SizedBox(width: 6),
                    Text('SUBMIT'),
                  ],
                ),
              ),
              const SizedBox(
                height: 20.0,
              ),
              TextButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =>
                            AllBlogs(messages: widget.messages),
                      ),
                    );
                  },
                  child: Text(
                    'All Titles',
                    style: GoogleFonts.aBeeZee(
                      fontSize: 60.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ))
            ],
          ),
        ),
      ),
    );
  }
} // LetUsChat state ends

As an outcome, we press the button, and reach a new page where we can see all the blog titles.

Material 3 and Flutter Firebase Provider Blog
Material 3 and Flutter Firebase Provider Blog

Showing all titles is not difficult. We have passed the blog collection which we retrieve from the Firestore database.

In the above code watch this part in particular.

final List<LetUsChatMessage> messages;
...
TextButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =>
                            AllBlogs(messages: widget.messages),
                      ),
                    );
                  },
                  child: Text(
                    'All Titles',
                    style: GoogleFonts.aBeeZee(
                      fontSize: 60.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ))

Meanwhile we can click any title and read the entire blog.

But before that we need to show them. Right? 

Material 3 and Flutter Firebase Provider Blog all titles
Material 3 and Flutter Firebase Provider Blog all titles

Let’s take a look at the full code. That’ll give you an idea of how we can use the for loop to select each title, and after that we have used a Gesture Detector Widget so that we can navigate and reach individual posts.

import 'dart:async';

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

import '../controller/all_widgets.dart';
import '../controller/authenticate_to_firebase.dart';
import '../model/state_of_application.dart';

class AllBlogs extends StatefulWidget {
  const AllBlogs({
    required this.messages,
  });

  final messages;

  @override
  State<AllBlogs> createState() => _AllBlogsState();
}

class _AllBlogsState extends State<AllBlogs> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            'Provider Firebase Blog',
            style: TextStyle(
              color: Theme.of(context).appBarTheme.foregroundColor,
            ),
          ),
        ),
        body: ListView(
          children: [
            for (var message in widget.messages)
              GestureDetector(
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => BlogDetailScreen(
                        name: message.name,
                        title: message.title,
                        body: message.body,
                      ),
                    ),
                  );
                },
                child: Paragraph('${message.name}: ${message.title}'),
              ),
          ],
        ));
  }
} // AllBlogs state ends

class BlogDetailScreen extends StatelessWidget {
  // static const routename = '/product-detail';

  const BlogDetailScreen({
    Key? key,
    required this.name,
    required this.title,
    required this.body,
  }) : super(key: key);
  final String name;
  final String title;
  final String body;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(name),
      ),
      body: SingleChildScrollView(
        child: Consumer<StateOfApplication>(
          builder: (context, appState, _) => Column(
            children: <Widget>[
              if (appState.loginState == UserStatus.loggedIn) ...[
                SizedBox(
                  height: 300,
                  width: double.infinity,
                  child: Image.network(
                    'https://cdn.pixabay.com/photo/2018/03/24/00/36/girl-3255402_960_720.png',
                    width: 250,
                    height: 250,
                    fit: BoxFit.cover,
                  ),
                ),
                const SizedBox(height: 10),
                Text(
                  title,
                  style: GoogleFonts.aBeeZee(
                    fontSize: 60.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 10),
                  width: double.infinity,
                  child: Text(
                    body,
                    textAlign: TextAlign.center,
                    softWrap: true,
                    style: GoogleFonts.aBeeZee(
                      fontSize: 30.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

Most importantly, each post shows the user’s name on the AppBar Widget.

And in the body section we see the title and content.

Material 3 and Flutter Firebase Provider Blog individual post page
Material 3 and Flutter Firebase Provider Blog individual post page

Do you want to clone the entire project and want to modify the code? 

Please clone this GitHub Repository.

What Next?

Books at Leanpub

Books in Apress

My books at Amazon

Courses at Educative

GitHub repository

Python and Data Science

Twitter

Comments

One response to “Material 3 : Flutter Firebase, Provider Blog App Final”

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

Leave a Reply