User Authentication with Flutter/Parse Stack

Doug Does Database
9 min readMar 23, 2023

--

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 a 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 = 'http://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 = "http://123.45.678.90/parse-dashboard"

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

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 configuration
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const Login(),
),
GoRoute(
path: '/todo',
builder: (context, state) => const Todo(),
),
],
);

And create a simple ToDo Landing page.

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.

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

User doesn’t have an account so Join Now.

Hit OK and land on ToDo screen.

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.

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 = 'http://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,
),
],
),
),
),
);
}
}

--

--

Doug Does Database

Developer Evangelist following all things Database and Cloud. Currently working on MBaas (Mobile Back End as a Service)