Flutter web 3.0 blog app with Firebase, Provider

While building the Flutter web 3.0 blog app with Firebase and Provider, we have faced some challenges.

Firstly, we cannot hard code the blog posts anymore. 

Secondly, we have to assure that only the signed-in visitors will post their blogs. 

Finally, we will not use the multi provider technique. Instead we will use the ChangeNotifierProvider class.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'model/state_of_application.dart';

import 'view/chat_app.dart';

/// moving to first branch

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => StateOfApplication(),
      builder: (context, _) => const ChatApp(),
    ),
  );
}

For instance, Flutter hard code is just like any other hard code principle that we cannot follow when we develop a dynamic flutter app. Right? 

Why?

Because in our case, we’re dealing with a remote database server Firebase and Firestore.

As a result, particularly at this stage, users will insert data, and retrieve data from Firebase.

On the contrary, we embed hard code into our source code. Neither it comes from any external source, nor we change the value on runtime. Right? 

For example, we’ve seen the same instance in our Firebase, Firestore and Provider web app.

What do we see on the screen?Let’s first see the home page of our flutter web app.

User can select a portion of text with selectable text in flutter
User can select a portion of text with selectable text in flutter

But after the initial change our current home page will look like the following.

Flutter web 3.0 homepage
Flutter web 3.0 homepage

As we see, here the first step is either you have to sign in, or you can register so that later you can sign in.

As an outcome, the Firebase authentication page will show the existing users who have registered already.

Flutter web 3.0 Firebase authentication page
Flutter web 3.0 Firebase authentication page

However, as we were discussing flutter 3.0 web app, initially it was different.

Why so? Because we had hard coded the initial blog data.

There was no flutter sign in method so that the user could log in.

It’s true that we had not introduced Firebase and Flutter sign in methods in our previous discussion.

How Flutter web 3.0 works with Firebase, and Provider

While talking about Flutter 3.0, we have seen what are the primary changes. 

Firstly, with reference to mobile application development, there has not been a great change. Structurally what we have been doing, will continue to do.

Secondly, we can work with Firebase and Provider just like before.

For that reason, we need to add the package dependencies first 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

As for the next move, we need to add the state management process in our model folder. 

This class will extend the Changenotifier class and initialize the state. 

Since our data source is external, we will use Future, async and awit.

In this class we will define different types of methods. It will check whether the user is new or existing. 

Based on that, new users can register as follows.

Flutter web 3.0 sign in, register page
Flutter web 3.0 sign in, register page

By the way, we can take a look at the code where it also checks whether the user is logged in or not.

On the other hand it will also add the blog posts to the Firebase Firestore collection.

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

import '../controller/authenticate_to_firebase.dart';
import '../firebase_options.dart';

import '../view/let_us_chat.dart';

class StateOfApplication extends ChangeNotifier {
  StateOfApplication() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = UserStatus.loggedIn;
        _chatBookSubscription = FirebaseFirestore.instance
            .collection('blog')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _chatBookMessages = [];
          for (final document in snapshot.docs) {
            _chatBookMessages.add(
              LetUsChatMessage(
                name: document.data()['name'] as String,
                title: document.data()['title'] as String,
                body: document.data()['body'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loginState = UserStatus.loggedOut;
        _chatBookMessages = [];
      }
      notifyListeners();
    });
  }

  UserStatus _loginState = UserStatus.loggedOut;
  UserStatus get loginState => _loginState;

  String? _email;
  String? get email => _email;

  StreamSubscription<QuerySnapshot>? _chatBookSubscription;
  StreamSubscription<QuerySnapshot>? get chatBookSubscription =>
      _chatBookSubscription;
  List<LetUsChatMessage> _chatBookMessages = [];
  List<LetUsChatMessage> get chatBookMessages => _chatBookMessages;

  void startLoginFlow() {
    _loginState = UserStatus.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = UserStatus.password;
      } else {
        _loginState = UserStatus.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = UserStatus.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }

  Future<DocumentReference> addMessageToChatBook(String title, String body) {
    if (_loginState != UserStatus.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance.collection('blog').add(<String, dynamic>{
      'title': title,
      'body': body,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
}

At the same time we also define the nature of the Flutter 3.0 application. 

While adding the project in Firebase, we need to choose the platform. According to that it creates the API keywords.

Now in our code we can supply those keywords and connect with Firebase.

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: '',
  );
}

Certainly, in our case, we have chosen the web platform.

As a result, now we can see the titles of the blogs.

Flutter web 3.0 all posts displaying titles
Flutter web 3.0 all posts displaying titles

Flutter web 3.0 and Firestore database

As our Flutter web 3.0 Firebase Provider blog app has built a connection with Firestore database, users can insert data. 

Once the data is into the external database, it is not difficult to navigate to the blog detail page.

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';
import 'let_us_chat.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider Firebase Blog'),
      ),
      body: ListView(
        children: <Widget>[
          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: 8),
          Consumer<StateOfApplication>(
            builder: (context, appState, _) => AuthenticationForFirebase(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Paragraph(
              'Hi, I\'m Angel, I\'m Inviting you to write Blogs. Please join me.'),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header('Write your Blog'),
          const Paragraph(
            'Join your friends and write your blog!',
          ),
          Consumer<StateOfApplication>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == UserStatus.loggedIn) ...[
                  TextButton(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => LetUsChat(
                            addMessageOne: (title, body) =>
                                appState.addMessageToChatBook(title, body),
                            messages: appState.chatBookMessages,
                          ),
                        ),
                      );
                    },
                    child: Text(
                      'Let\'s Blog',
                      style: GoogleFonts.laila(
                        fontSize: 30.0,
                        fontWeight: FontWeight.bold,
                        color: Colors.yellow,
                        backgroundColor: Colors.red,
                      ),
                    ),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Before we have done a lot of such things.

Sending data through the class constructor.

But it would not be possible, if we had not used ChangeNotifierProvider, ChangeNotifier, and Consumer from the Provider package.

After all, we are sending data from the parent widget to the child widget. In addition, we need to manage the state of the application. 

Incidentally the child widget will now consume the data and display the blog detail page.

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 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
  _LetUsChatState 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: const Text('Provider Firebase Blog'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Expanded(
                child: TextFormField(
                  controller: _controllerOne,
                  decoration: const InputDecoration(
                    hintText: 'title',
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Enter your message to continue';
                    }
                    return null;
                  },
                ),
              ),
              const SizedBox(width: 10),
              Expanded(
                child: TextFormField(
                  controller: _controllerTwo,
                  decoration: const InputDecoration(
                    hintText: 'Body',
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Enter your message to continue';
                    }
                    return null;
                  },
                ),
              ),
              const SizedBox(width: 10),
              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'),
                  ],
                ),
              ),
              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}'),
                ),
            ],
          ),
        ),
      ),
    );
  }
} // LetUsChat 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,
                    color: Colors.yellow,
                    backgroundColor: Colors.red,
                  ),
                ),
                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,
                      color: Colors.black26,
                      backgroundColor: Colors.lightBlue[300],
                    ),
                  ),
                ),
                const SizedBox(
                  height: 10,
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

Finally we can see the blog that Angel has just posted.

Flutter web 3.0 blog detail page
Flutter web 3.0 blog detail page

At the same time the same blog post is present at the Firestore database collection.

Flutter web 3.0 Firebase Firestore database shows the blog posts
Flutter web 3.0 Firebase Firestore database shows the blog posts

However, we are not happy with the design part. Therefore we will introduce Material Design 3 and change the entire look of the Flutter web 3.0 Firebase Provider blog app.

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

5 responses to “Flutter web 3.0 blog app with Firebase, Provider”

  1. […] addition, the StatefulWidget as an ancestor manages the controller’s […]

  2. […] In Flutter 3.0 theme color we’ll use Material design 3. Certainly we will adopt the same principle in our ongoing web app. […]

  3. […] We’ve been building a web app where users can write their blogs and share with other members. However, initially we have used the Material design 2 theme. […]

  4. […] 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. […]

  5. […] our previous Firebase, Provider Blog app section, we have used Material Design 3 to customise the theme. However, at the entry point of our […]

Leave a Reply