Fluttering With Me: My First App - Part 1
Tags: webdev, flutter, beginners, tutorial
Published: 2023-03-02 02:59:21

`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](https://docs.flutter.dev/get-started/install/macos)
[Flutter/Firebase Setup](https://firebase.google.com/docs/flutter/setup?platform=ios)

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:

- [cmdline-tools missing](https://stackoverflow.com/questions/68236007/i-am-getting-error-cmdline-tools-component-is-missing-after-installing-flutter)
- [Unable To Find Java Version](https://stackoverflow.com/questions/68569430/unable-to-find-bundled-java-version-with-flutter-doctor-after-updating-android)

(Note: for the second error, I copied the `jbr` folder, renamed it to `jre`, and the error was resolved.)
![Folders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5gbeea3pt5ld43es9bkr.png)

1. Creating the project. ```bash
flutter create --org com.yourdomainname mynotes
```


2. Adding Firebase. In the newly created project folder, use the following command in the terminal: ```bash
flutter pub add firebase_core
```

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

```yaml
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
```

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.

```dart
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()
    },
  ));
}
```

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.

```dart
const loginRoute = '/login/';
const registerRoute = '/register/';
const notesRoute = '/notes/';
const verifyEmailRoute = '/verifyEmail/';
```

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

```dart
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();
        }
      },
    );
  }
}

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kz78q2ksx9jz5zbhl14m.png)


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`.


```dart
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.'))
        ],
      ),
    );
  }
}

```
**`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 `_password `controllers 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: `_email `and `_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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plu18q40swsfbkpgenpj.gif)

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

---

##Login

![Login Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43775e137g7zzx3uuioy.png)


```dart
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!'))
          ],
        ),
      ),
    );
  }
}

```

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.

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

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t3ta2gri1e6rinh20yu9.gif)



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`

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

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ynvut9441e015hyi3ak.gif)