User Authentication  with Flutter/Parse Stack

User Authentication with Flutter/Parse Stack

The following code demonstrates implementing user authentication in a Flutter Application. But first, a few words about the stack this application is built against and how to connect the Flutter App to the backend server. This article assumes basic Flutter knowledge. The complete code base is at the end of the article.


A few words about the stack

The server side is based on the Oracle Backend for Parse Platform. This is platform that consists of a Parse Server being installed to a Kubernetes Cluster. An Autonomous Database instance is provisioned and the Parse Servers are connected upon startup.

One the stack is has completed provisioning, the Flutter App developer needs 2 configuration parameters to connect to the Parse Server.

parse_application_id = "APPLICATION_ID
parse_endpoint = "123.45.678.90/parse""        

These are taken from the Oracle Backend for Parse Platform installation log.

Installing Oracle Backend for Parse Platform


Connect the Flutter App

Article on connecting a Flutter Application to a Parse server

The article above had used an early version of the Dart and Flutter packages. These had a major revision to 4. Follow the instructions to install the dependencies in your Flutter project.

There is a Dart type and a Flutter type. What's the difference?

Flutter framework is an open-source UI SDK. Dart language, on the other hand, is an open-source, client-side programming platform.

Install them both.

The Parse initialization code is in the main() method of main.dart

void main() async {
  //  Parse code
  WidgetsFlutterBinding.ensureInitialized();

  // Parse Config Options 
  const keyApplicationId = 'APPLICATION_ID';
  const keyParseServerUrl = 'https://123.45.678.90/parse';

  // Connect to Parse Server
  var response = await Parse().initialize(keyApplicationId, keyParseServerUrl);
  if (!response.hasParseBeenInitialized()) {
    // https://stackoverflow.com/questions/45109557/flutter-how-to-programmatically-exit-the-app
    exit(0);
  }

  // Create an Object to verify connected
  var firstObject = ParseObject('FirstClass')
    ..set('message', 'Parse Login Demo is Connected');
  await firstObject.save();

  print('done');
  // Parse code done
  runApp(const MyApp());
}        

That is all that is required to connect from a new Flutter Project. Run it within Visual Code and verify that the FirstClass collection has been created and the document has been saved. Use the Parse Dashboard from the stack, it is also taken from the installation log.

parse_dashboard_uri = "https://123.45.678.90/parse-dashboard"        
No alt text provided for this image

Server is connected, lets write the the login screen. But first let's set up GoRouter.


GoRouter

GoRouter is a declarative routing package for Flutter that uses the Router API to provide a convenient, url-based API for navigating between different screens

Add a GoRouter to Main.dart to route to the login screen upon startup

// GoRouter configuration
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const Login(),
    ),
  ],
);        

And tell MyApp to use it

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


  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}        



Login


Write the Login screen

No alt text provided for this image

On Pressing the Login Button.

child: ElevatedButton(
  child: const Text('Login'),
  onPressed: () {
    print("Controller name = ${nameController.text}");
    print(
        "Controller password = ${passwordController.text}");
    processLogin(context, nameController.text,
        passwordController.text);
  },
)),        

Call processLogin to verify credentials on the backend.

  processLogin(context, username, password) async {
    print("processLogin name = $username");
    print("processLogin password = $password");
    
    // Create ParseUser and call login
    var user = ParseUser(username, password, "");
    var response = await user.login();


    if (response.success) {
      // If successful, Route to the ToDo Landing page
      user = response.result;
      GoRouter.of(context).go('/todo');
    } else {
      // set up the button
      Widget okButton = TextButton(
        child: const Text("OK"),
        onPressed: () => Navigator.pop(context),
      );
      // set up the AlertDialog with Error Message
      AlertDialog alert = AlertDialog(
        title: const Text("Error Dialog"),
        content: Text('${response.error?.message}'),
        actions: [
          okButton,
        ],
      );


      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    }
  }        

On success, we route the validated user to a "Home" page. In this example, it is ToDo so we need a ToDo landing page to route to upon successful login

GoRouter.of(context).go('/todo');        

ToDo

First, like before, add the ToDo route to Main.dart

// GoRouter configuratio
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const Login(),
    ),
    GoRoute(
      path: '/todo',
      builder: (context, state) => const Todo(),
    ),
  ],
);n        

And create a simple ToDo Landing page

No alt text provided for this image

There is nothing Parse related here. Its a simple screen, code can be found at the end of the article.

But we first must add the user by implementing a SignUp Screen


SignUp

The login screen had a Join Us Now link which, when clicked, invoked the SignUp page.

children: <Widget> [
  const Text('Need an account?'),
  TextButton(
    child: const Text(
      'Join us now',
      //style: TextStyle(fontSize: 20),
    ),
    onPressed: () {
      //signup screen
      GoRouter.of(context).go('/signup');
    },
  )
],        

Like before, update GoRouter in Main.dart for Signup screen.

// GoRouter configuration
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const Login(),
    ),
    GoRoute(
      path: '/todo',
      builder: (context, state) => const Todo(),
    ),
    GoRoute(
      path: '/signup',
      builder: (context, state) => const SignUp(),
    ),
  ],
);        

And implement SignUp screen.

No alt text provided for this image

Like Login, when the user presses CreateAccount, processSignUp() function is called which interacts with backend server

child: ElevatedButton(
  child: const Text('Create Account'),
  onPressed: () {
    print(
        "Controller user name = ${usernameController.text}");
    print(
        "Controller password = ${passwordController.text}");
    print("Controller password = ${emailController.text}");
    processSignUp(context, usernameController.text,
        passwordController.text, emailController.text);
  },        

And processSignUp()

  processSignUp(context, username, password, email) async 
    print("processSignUp name = $username");
    print("processSignUp password = $password");
    print("processSignUp email = $email");

    // Create Parse User and call create() 
    var user = ParseUser(username, password, "");
    var response = await ParseUser(username, password, email).create();

    if (response.success) {
      // Show Alert that Account was created
      // On OK Pressed, route to Landing page, ToDo
      user = response.result;
      print("user = $user");
      // set up the button
      Widget okButton = TextButton(
          child: const Text("OK"),
          onPressed: () {
            GoRouter.of(context).go('/todo');
          });
      // set up the AlertDialog
      AlertDialog alert = AlertDialog(
        title: const Text("Success"),
        content: const Text("Account successfully created"),
        actions: [
          okButton,
        ],
      );


      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    } else {
      // Display Error Message
      // set up the button
      Widget okButton = TextButton(
        child: const Text("OK"),
        onPressed: () => Navigator.pop(context),
      );
      // set up the AlertDialog
      AlertDialog alert = AlertDialog(
        title: const Text("Error Dialog"),
        content: Text('${response.error?.message}'),
        actions: [
          okButton,
        ],
      );


      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    }        

Build and Run

Login with credentials, User = New and Password = Client

No alt text provided for this image

User doesn't have an account so Join Now

No alt text provided for this image

Hit OK and land on ToDo screen

No alt text provided for this image

In Visual Code, hit the Hot Reload button which will go back to the Login page and login with the newly added user.

Look at the Parse Dashboard to verify the user as added

No alt text provided for this image



Summary and Code

That's it. A small Flutter app that implement User Authentication via Parse Server. Pretty Simple.

In the future, i plan to have articles regarding using Access Control Lists, OAuth and Email Verification

And now the code

Main.dart

import 'dart:io';


import 'package:flutter/material.dart';
import 'package:parse_login_demo/screens/login.dart';
import 'package:parse_login_demo/screens/signup.dart';
import 'package:parse_login_demo/screens/todo.dart';
import 'package:parse_server_sdk/parse_server_sdk.dart';


import 'package:go_router/go_router.dart';


void main() async {
  //  Parse code
  WidgetsFlutterBinding.ensureInitialized();

  // Parse Config Options 
  const keyApplicationId = 'APPLICATION_ID';
  const keyParseServerUrl = 'https://123.45.678.90/parse';

  // Connect to Parse Server
  var response = await Parse().initialize(keyApplicationId, keyParseServerUrl);
  if (!response.hasParseBeenInitialized()) {
    // https://stackoverflow.com/questions/45109557/flutter-how-to-programmatically-exit-the-app
    exit(0);
  }

  // Create an Object to verify connected
  var firstObject = ParseObject('FirstClass')
    ..set('message', 'Parse Login Demo is Connected');
  await firstObject.save();

  print('done');
  // Parse code done
  runApp(const MyApp());
}


// GoRouter configuration
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const Login(),
    ),
    GoRoute(
      path: '/todo',
      builder: (context, state) => const Todo(),
    ),
    GoRoute(
      path: '/signup',
      builder: (context, state) => const SignUp(),
    ),
  ],
);


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


  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}
        

Login.dart

import 'package:flutter/material.dart';


import 'package:parse_server_sdk/parse_server_sdk.dart';
import 'package:go_router/go_router.dart';


class Login extends StatefulWidget {
  const Login({Key? key}) : super(key: key);


  @override
  State<Login> createState() => _LoginState();
}


class _LoginState extends State<Login> {
  TextEditingController nameController = TextEditingController();
  TextEditingController passwordController = TextEditingController();


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('LoginDemo'),
        ),
        body: Padding(
            padding: const EdgeInsets.all(10),
            child: ListView(
              children: <Widget>[
                Container(
                    alignment: Alignment.center,
                    padding: const EdgeInsets.all(10),
                    child: const Text(
                      'My App',
                      style: TextStyle(
                          color: Colors.blue,
                          fontWeight: FontWeight.w500,
                          fontSize: 30),
                    )),
                Container(
                    alignment: Alignment.center,
                    padding: const EdgeInsets.all(10),
                    child: const Text(
                      'Sign in',
                      style: TextStyle(fontSize: 20),
                    )),
                Container(
                  padding: const EdgeInsets.all(10),
                  child: TextField(
                    controller: nameController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'User Name',
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
                  child: TextField(
                    obscureText: true,
                    controller: passwordController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'Password',
                    ),
                  ),
                ),
                TextButton(
                  onPressed: () {
                    //forgot password screen
                  },
                  child: const Text(
                    'Forgot Password',
                  ),
                ),
                Container(
                    height: 50,
                    padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
                    child: ElevatedButton(
                      child: const Text('Login'),
                      onPressed: () {
                        print("Controller name = ${nameController.text}");
                        print(
                            "Controller password = ${passwordController.text}");
                        processLogin(context, nameController.text,
                            passwordController.text);
                      },
                    )),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    const Text('Need an account?'),
                    TextButton(
                      child: const Text(
                        'Join us now',
                        //style: TextStyle(fontSize: 20),
                      ),
                      onPressed: () {
                        //signup screen
                        print("SIGNUP PRESSED");
                        GoRouter.of(context).go('/signup');
                      },
                    )
                  ],
                ),
              ],
            )));
  }


  processLogin(context, username, password) async {
    print("processLogin name = $username");
    print("processLogin password = $password");
    var user = ParseUser(username, password, "");
    var response = await user.login();
    if (response.success) {
      user = response.result;
      GoRouter.of(context).go('/todo');
    } else {
      // set up the button
      Widget okButton = TextButton(
        child: const Text("OK"),
        onPressed: () => Navigator.pop(context),
      );
      // set up the AlertDialog
      AlertDialog alert = AlertDialog(
        title: const Text("Error Dialog"),
        content: Text('${response.error?.message}'),
        actions: [
          okButton,
        ],
      );


      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    }
  }
}
        

Signup.dart

import 'package:flutter/material.dart'
import 'package:parse_server_sdk/parse_server_sdk.dart';
import 'package:go_router/go_router.dart';

class SignUp extends StatefulWidget {
  const SignUp({
    Key? key,
  }) : super(key: key);

  @override
  State<SignUp> createState() => _SignUpState();
}

class _SignUpState extends State<SignUp> {
  @override
  void initState() {
    super.initState();
  }

  TextEditingController usernameController = TextEditingController();
  TextEditingController passwordController = TextEditingController();
  TextEditingController emailController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Account SignUp")),
      body: Center(
        child: Container(
            padding: const EdgeInsets.all(16),
            child: ListView(
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.all(10),
                  child: TextField(
                    controller: usernameController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'User Name',
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.all(10),
                  child: TextField(
                    controller: passwordController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'User Password',
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
                  child: TextField(
                    controller: emailController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      labelText: 'User Email',
                    ),
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                Container(
                    height: 50,
                    padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
                    child: ElevatedButton(
                      child: const Text('Create Account'),
                      onPressed: () {
                        print(
                            "Controller user name = ${usernameController.text}");
                        print(
                            "Controller password = ${passwordController.text}");
                        print("Controller password = ${emailController.text}");
                        processSignUp(context, usernameController.text,
                            passwordController.text, emailController.text);
                      },
                    )),
              ],
            )),
      ),
    );
  }

  processSignUp(context, username, password, email) async {
    print("processSignUp name = $username");
    print("processSignUp password = $password");
    print("processSignUp email = $email");
    var user = ParseUser(username, password, "");
    var response = await ParseUser(username, password, email).create();
    if (response.success) {
      user = response.result;
      print("user = $user");
      // set up the button
      Widget okButton = TextButton(
          child: const Text("OK"),
          onPressed: () {
            GoRouter.of(context).go('/todo');
          });
      // set up the AlertDialog
      AlertDialog alert = AlertDialog(
        title: const Text("Success"),
        content: const Text("Account successfully created"),
        actions: [
          okButton,
        ],
      );

      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    } else {
      // set up the button
      Widget okButton = TextButton(
        child: const Text("OK"),
        onPressed: () => Navigator.pop(context),
      );
      // set up the AlertDialog
      AlertDialog alert = AlertDialog(
        title: const Text("Error Dialog"),
        content: Text('${response.error?.message}'),
        actions: [
          okButton,
        ],
      );

      // show the dialog
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return alert;
        },
      );
    }
  }
}
;        

Todo.dart

import 'package:flutter/material.dart';

class Todo extends StatefulWidget {
  const Todo({Key? key}) : super(key: key);

  @override
  State<Todo> createState() => _TodoState();
}

class _TodoState extends State<Todo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("ToDo")),
      body: Center(
        child: Container(
          padding: const EdgeInsets.all(16),
          child: ListView(
            children: const [
              Text(
                'ToDo List goes here',
                style: TextStyle(
                  color: Colors.black,
                  fontWeight: FontWeight.w800,
                  fontFamily: 'Roboto',
                  letterSpacing: 0.5,
                  fontSize: 20,
                ),
              ),
              SizedBox(
                height: 20,
              ),
            ],
          ),
        ),
      ),
    );
  }
}        
Siddharth Mehta

Freelance Frontend Developer | React, Angular & UI/UX Enthusiast

1 年

Doug Drechsel Instead of using Username and Password Login method. you can try out otpless.com/comment. Effortless integration, free forever. OTP-less one-tap login with Google, Apple, and WhatsApp offers several benefits, including increased convenience, improved security.

回复

要查看或添加评论,请登录

Doug Drechsel的更多文章

社区洞察

其他会员也浏览了