DEV Community

Cover image for Fluttering With Me: My First App - Part 1
Mattias Velamsson
Mattias Velamsson

Posted on

Fluttering With Me: My First App - Part 1

Hey!

Recently, I had a chat with a friend about mobile development and Flutter, and the more I learned about it, the more interested I became. I decided to try it for myself and document my journey of creating my first app through a blog post. (spoiler: I'm hooked...)

The app that I will be building is a notetaking app, and it will evolve visually and functionally with each update.


Setup ⚙️

Setting up Flutter was both similar and different from other technologies I have used before. Although the syntax was different, it followed the same routine.

Since I am using Firebase as a backend for this project, installation and setup instructions are included.

Setting up Flutter and Firebase

To set up Flutter and Firebase, I followed these instructions:

Flutter - Get Started - MacOS
Flutter/Firebase Setup

I used Flutter Doctor in between installations to ensure everything was okay before moving on.

Here are two errors I encountered during the Flutter setup and their solutions:

(Note: for the second error, I copied the jbr folder, renamed it to jre, and the error was resolved.)
Folders

  1. Creating the project.

    flutter create --org com.yourdomainname mynotes
    
  2. Adding Firebase. In the newly created project folder, use the following command in the terminal:

    flutter pub add firebase_core
    

Once everything is installed, the pubspec.yaml should include the following dependencies:

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: ^2.6.1
  firebase_auth: ^4.2.8
  cloud_firestore: ^4.4.2
  firebase_analytics: ^10.1.3
Enter fullscreen mode Exit fullscreen mode

I customized my project to support Android and iOS. However, for this project, I will only be working with Android as I do not have an Apple Developer Account.

Now that setup is complete, let's move on to the fun stuff!


Home

For the Home view, I will be working directly from main.dart, as this is where MaterialApp is by default, and where the app initializes.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(MaterialApp(
    title: 'Home',
    theme: ThemeData(
      primarySwatch: Colors.orange,
    ),
    home: const LoginView(),
    routes: {
      loginRoute: (context) => const LoginView(),
      registerRoute: (context) => const RegisterView(),
      notesRoute: (context) => const NotesView(),
      verifyEmailRoute: (context) => const VerifyEmailView()
    },
  ));
}
Enter fullscreen mode Exit fullscreen mode

In our main() function, I initialized Firebase to be able to manage authentication later on. I made it async to make it wait for the response from Firebase.initializeApp() before moving on. In the MaterialApp, I modified the color slightly to Orange and set LoginView() to be the entry-point.

To navigate between views, I added routes that can be easily called later. The routes are imported via lib/views/constants/routes.dart that I created.

const loginRoute = '/login/';
const registerRoute = '/register/';
const notesRoute = '/notes/';
const verifyEmailRoute = '/verifyEmail/';
Enter fullscreen mode Exit fullscreen mode

Now, let's go through the code for HomePage.

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.done:
            final user = FirebaseAuth.instance.currentUser;
            if (user != null) {
              if (user.emailVerified) {
                return const NotesView();
              } else {
                return const VerifyEmailView();
              }
            } else {
              return const LoginView();
            }
          default:
            return const CircularProgressIndicator();
        }
      },
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

In here we do a bunch of things.

The HomePage is actually just checking the authentication state of the user. If the user is authenticated, the NotesView() is displayed, otherwise the LoginView() is displayed.

To check the user's authentication state, we're using FirebaseAuth.instance.authStateChanges() which returns a Stream<User?>. If the connection is done, we can get the current user with FirebaseAuth.instance.currentUser, if there is one. If not, we'll show the LoginView().

So to round up the logic:

  • There is a user => NotesView();
  • User is Authenticated and Verified => NotesView();
  • Any other situation => LoginView();

The CircularProgressIndicator() is just displayed while waiting for the connection to be done.

That's all for HomePage!

Registration

To be able to login, we first need to register - right? Let's implement this!

RegistrationView

To implement the registration view, we create a new file called register_view.dart in the lib/views folder. This file contains a stateful widget class called RegisterView.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:mynotes/utilities/show_error_dialog.dart';

import 'package:mynotes/views/constants/routes.dart';

class RegisterView extends StatefulWidget {
  const RegisterView({super.key});

  @override
  State<RegisterView> createState() => _RegisterViewState();
}

class _RegisterViewState extends State<RegisterView> {
  late final TextEditingController _email;
  late final TextEditingController _password;

  @override
  void initState() {
    _email = TextEditingController();
    _password = TextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    _email.dispose();
    _password.dispose();
    super.dispose();
  }
 Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Register!'),
      ),
      body: Column(
        children: [
          TextField(
              controller: _email,
              enableSuggestions: false,
              autocorrect: false,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(hintText: 'Enter your email')),
          TextField(
            controller: _password,
            obscureText: true,
            enableSuggestions: false,
            autocorrect: false,
            decoration: const InputDecoration(hintText: 'Enter your password'),
          ),
          TextButton(
            onPressed: () async {
              final email = _email.text;
              final password = _password.text;
              try {
                final userCredential =
                    await FirebaseAuth.instance.createUserWithEmailAndPassword(
                  email: email,
                  password: password,
                );
                Navigator.of(context).pushNamed(verifyEmailRoute);
                final user = FirebaseAuth.instance.currentUser;
                await user?.sendEmailVerification();
              } on FirebaseAuthException catch (e) {
                showErrorDialog(context, e.code.toString());
              } catch (e) {
                showErrorDialog(context, e.toString());
              }
            },
            child: const Text('Register'),
          ),
          TextButton(
              onPressed: () {
                Navigator.of(context)
                    .pushNamedAndRemoveUntil(loginRoute, (route) => false);
              },
              child: const Text('Already registered? Login here.'))
        ],
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

TextEditingController
The widget has two TextEditingController objects named _email and _password. These controllers are used to keep what the user enters in the TextField widgets.

initState
This is called when the widget first gets created. It basically just initializes _email and _password with empty strings.

dispose
This one was a bit tricky to understand for me at first. But this basically disposes/removes the _email and _passwordcontrollers once our widgets are removed from the tree(view changes).

build and Scaffold
Here we're building the actual container/skeleton of for how the view should look. The Scaffold is basically the frame holding everything, and then we add other Widgets such as AppBar, TextField, TextButton etc.

TextField
We use these widgets to get the input from the user. These fields are then connected to each of controllers created earlier: _emailand _password

TextButton
The first TextButton widget is the registration button. When pressed, it attempts to create a new user account using the entered email and password.

  • Successful creation => verifyEmailView();
  • Unsuccessful creation => Displays an error popup with the error message.

Here's the entire process of how it looks in Firebase's Console as well.

Account Creation Process

That's all for Register, now lets check the LoginView!


Login

Login Page

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'dart:developer' as devtools show log;

import 'package:mynotes/views/constants/routes.dart';

import '../utilities/show_error_dialog.dart';

class LoginView extends StatefulWidget {
  const LoginView({super.key});

  @override
  State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends State<LoginView> {
  late final TextEditingController _email;
  late final TextEditingController _password;

  @override
  void initState() {
    _email = TextEditingController();
    _password = TextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    _email.dispose();
    _password.dispose();
    super.dispose();
  }

  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: const Text('Login 🔑'),
      ),
      body: Container(
        decoration: const BoxDecoration(
            image: DecorationImage(
                image: NetworkImage(
                    'https://static.vecteezy.com/system/resources/previews/009/376/704/original/abstract-dark-orange-blob-element-free-png.png'),
                fit: BoxFit.contain,
                alignment: Alignment(1, 1))),
        child: Column(
          children: [
            TextField(
              controller: _email,
              enableSuggestions: false,
              autocorrect: false,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                hintText: 'Enter your email',
              ),
            ),
            TextField(
              controller: _password,
              obscureText: true,
              enableSuggestions: false,
              autocorrect: false,
              decoration: const InputDecoration(
                hintText: 'Enter your password',
              ),
            ),
            TextButton(
              onPressed: () async {
                final email = _email.text;
                final password = _password.text;
                try {
                  final userCredential =
                      await FirebaseAuth.instance.signInWithEmailAndPassword(
                    email: email,
                    password: password,
                  );
                  final user = FirebaseAuth.instance.currentUser;
                  if (user?.emailVerified ?? false) {
                    Navigator.of(context).pushNamedAndRemoveUntil(
                      notesRoute,
                      (route) => false,
                    );
                  } else {
                    Navigator.of(context).pushNamedAndRemoveUntil(
                        verifyEmailRoute, (route) => false);
                  }
                  devtools.log(userCredential.toString());
                } on FirebaseAuthException catch (e) {
                  await showErrorDialog(context, (e.code.toString()));
                } catch (e) {
                  await showErrorDialog(context, e.toString());
                }
              },
              child: const Text('Login'),
            ),
            TextButton(
                onPressed: () {
                  Navigator.of(context)
                      .pushNamedAndRemoveUntil(registerRoute, (route) => false);
                },
                child: const Text('Not registered? Click here!'))
          ],
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Similar to the RegisterView() we have controllers for our email_ and _password so I won't be going through these again.

The important thing on this page is how we handle the Login itself with the FirebaseAuth.

First, we assign the result of FirebaseAuth.instance.signInWithEmailAndPassword() to a object called UserCredential.

We then run some logic:

  • User is verified => notesView()
  • User not verified => verifyEmailView()

If there are any errors, it will show an error popup similar to the one on the RegisterView.

The last button directly navigates to the RegisterView().

That's about it for the Login. But how about Logging out?


NotesView & Logout

This one was a bit tricky to understand at first, as you generally want a logout option to be easily accessible. For now I decided to try adding actions in the AppBar on the notesView and use that for logging out.

enum MenuAction { logout }

class NotesView extends StatefulWidget {
  const NotesView({super.key});

  @override
  State<NotesView> createState() => _NotesViewState();
}

class _NotesViewState extends State<NotesView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Notes 📝'),
        actions: [
          PopupMenuButton<MenuAction>(
            onSelected: (value) async {
              switch (value) {
                case MenuAction.logout:
                  final shouldLogout = await showLogOutDialog(context);
                  if (shouldLogout) {
                    await FirebaseAuth.instance.signOut();
                    Navigator.of(context)
                        .pushNamedAndRemoveUntil(loginRoute, (_) => false);
                  }
              }
            },
            itemBuilder: (context) {
              return const [
                PopupMenuItem<MenuAction>(
                    value: MenuAction.logout, child: Text('Logout')),
                PopupMenuItem<MenuAction>(child: Text('Second Page'))
              ];
            },
          )
        ],
      ),
      body: const Text('Hello NotesView'),
    );
  }
}

Future<bool> showLogOutDialog(BuildContext context) {
  return showDialog<bool>(
    context: context,
    builder: (context) {
      return AlertDialog(
          title: const Text('Sign Out Alert'),
          content: const Text('Are you sure you want to sign out?'),
          actions: [
            TextButton(
                onPressed: () {
                  Navigator.of(context).pop(false);
                },
                child: const Text('Cancel')),
            TextButton(
                onPressed: () {
                  Navigator.of(context).pop(true);
                },
                child: const Text('Logout'))
          ]);
    },
  ).then((value) => value ?? false);
}

Enter fullscreen mode Exit fullscreen mode

First we define an enum MenuAction which has the value logout. This is to define the options in our PopupMenu in the AppBar.

In the AppBar of NotesView we add the Actions widget that contains a PopupMenuButton. The PopupMenuButton is created with a list of PopupMenuItem widgets. In this case, there are two items, but only one with the value MenuAction.logout, which obviously is for logging out.

onSelected
As it's name, when the user selects an item in the menu. In this case when the user selects the MenuAction.logout we will show an message confirming that the user really want to log out.

If they confirm, we call FirebaseAuth.instance.sigOut() to log out the user and then push them over to the LoginView again.

showLogOutDialog
As mentioned earlier, this shows a popup that requests the user confirm that they want to logout. The function returns a Future<bool>. true if they confirm, or false if they cancel and want to stay logged in.

But why is the showLogOutDialog a "Future"?

The easiest way I can explain, and how I understand it is:

a value that will be available at some point in the future, but not necessarily immediately.

In our case, this would be when/if the user clicks Confirm/Cancel in the popout.

And as Futures are non-blocking, it also lets the app keep running in the background while Future is waiting for it's value. Basically a way to perform async operations and make the app responsive while loading data.

Logging Out

That's about it!

Last thing I will go through is the showErrorDialog() which I've been using throughout the app.


showErrorDialog

lib/utilities/show_error_dialog.dart

import 'package:flutter/material.dart';

Future<void> showErrorDialog(
  BuildContext context,
  String text,
) {
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('An error occured'),
        content: Text(text),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text('Ok'),
          ),
        ],
      );
    },
  );
}

Enter fullscreen mode Exit fullscreen mode

Just as the logout popup, this is also a Future object as it is waiting for the value of errors that might happen.

Overall, this function can be called whenever there is an error that needs to be displayed to the user in a pop-up dialog box.


And that's it for Part 1. Phew! That was quite a lot. It's been insanely fun building with Flutter so far, and I'm definitely going to dig a lot deeper into it and continue on this app. Hopefully Part 2 will be up next week 🤞

Here is the current final result.

Final Result P1

Top comments (0)