Login Flow in Flutter

In this article, I will be showing a simple flutter app that will demonstrate a typical mobile login flow.

This article is not an introductory tutorial on flutter or programming. I am assuming that the reader knows programming and has basic understanding of how flutter works. You can find more from flutter.io.

Suggested laptop for programming
Lenovo IdeaPad S145 AMD Ryzen 5 15.6" FHD Thin and Light Laptop (8GB/512GB SSD/Windows10/Office/Platinum Grey/1.85Kg)

Buy on Amazon

Suggested readings

This app will do the following simple things:

  • allow a user to login using email as the username and a password
  • post login, show a simple landing screen. The landing screen will display the username entered earlier.
  • show the landing screen to a already logged in user bypassing the login screen.

Lets start building this app. Create a flutter app and name it say login_flow. This will generate a bunch of files. Open the lib/main.dart file. The main.dart file is our entry point for the app. Replace the contents with the following:

import 'package:flutter/material.dart';
import 'package:login_navigation_sample/screens/login.dart';
import 'package:login_navigation_sample/screens/home.dart';
import 'package:login_navigation_sample/landing.dart';


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login Demo',
      routes: {
        '/': (context) => Landing(),
        '/login': (context) => Login(),
        '/home': (context) => MyHomePage(title: 'Login Demo'),
      },
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
    );
  }
}

I am using MaterialApp class to bootstrap the app. This class wraps a number of common widgets required for a material app. We are giving our app a title of Login Demo, giving it a material theme of green colour and also initialising the apps’ top-level routes.

The first route "/" is the default one - meaning the class mapped to this will be loaded first when the app is opened.

Lets now code the landing class.

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class Landing extends StatefulWidget {
  @override
  _LandingState createState() => _LandingState();
}

class _LandingState extends State<Landing> {
  String _username = "";

  @override
  void initState() {
    super.initState();
    _loadUserInfo();
  }

  _loadUserInfo() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    _username = (prefs.getString('username') ?? "");
    if (_username == "") {
      Navigator.pushNamedAndRemoveUntil(
          context, '/login', ModalRoute.withName('/login'));
    } else {
      Navigator.pushNamedAndRemoveUntil(
          context, '/home', ModalRoute.withName('/home'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: CircularProgressIndicator()));
  }
}

The only widget we have in this screen is a circular progress icon. However, we have overriden the initState() method to check if a user is already logged in or not.

The package shared_preferences is being used here to access the persistent store for simple data. This can be used both for android and ios.

We are looking for data with the key username. If its present, we assume that the user is logged in and we navigate to the home screen else we navigate to the login screen. Notice that we are using Navigator.pushNamedAndRemoveUntil here instead of Navigate.push. This is becauase we do not want to give the user the ability to navigate back to the landing screen from either login or home screen. If Navigate.push is used then a back arrow will be available in the toolbar for the user to navigate back.

Now lets look at login screen. Here is the code.

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class Login extends StatefulWidget {
  @override
  _LoginState createState() => _LoginState();
}

class _LoginState extends State<Login> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  bool _autovalidate = false;
  String _email, _password;

  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  void showInSnackBar(String value) {
    _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
  }

  void _handleSubmitted() async {
    final FormState form = _formKey.currentState;
    if (!form.validate()) {
      _autovalidate = true; // Start validating on every change.
      showInSnackBar('Please fix the errors in red before submitting.');
    } else {
      form.save();
      if ( _password == "password") {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        prefs.setString("username", _email);
        Navigator.pushNamedAndRemoveUntil(
            context, '/home', ModalRoute.withName('/home'));
      } else {
        showInSnackBar('Incorrect credentials');
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(
          title: Text('Login'),
        ),
        body: SafeArea(
          top: false,
          bottom: false,
          child: Form(
            key: _formKey,
            child: SingleChildScrollView(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  TextFormField(
                    key: Key("_mobile"),
                    decoration: InputDecoration(labelText: "Email"),
                    keyboardType: TextInputType.emailAddress,
                    onSaved: (String value) {
                      _email = value;
                    },
                    validator: (value) {
                      if (value.isEmpty) {
                        return 'Email is required';
                      }
                    },
                  ),
                  TextFormField(
                    decoration: InputDecoration(labelText: "Password"),
                    obscureText: true,
                    onSaved: (String value) {
                      _password = value;
                    },
                    validator: (value) {
                      if (value.isEmpty) {
                        return 'Password is required';
                      }
                    },
                  ),
                  const SizedBox(height: 10.0),
                  ButtonBar(
                    children: <Widget>[
                      RaisedButton.icon(
                          onPressed: _handleSubmitted,
                          icon: Icon(Icons.arrow_forward),
                          label: Text('Sign in')),
                    ],
                  )
                ],
              ),
            ),
          ),
        ));
  }
}

This screen has a username field, a password field and a sign in button. We have a function _handleSubmit that gets called on pressing the sign in button. The _handleSubmit method simply checks if the password entered is "password" and if the comparision is successful, takes the user to the home screen. In a realistic app, we would have validted the login information using some sort of a backend. The method then saves the entered username into the persistent store using the sharedpreferences package.

Finally, lets look at the home screen. Here is the code.

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _username;

  @override
  void initState() {
    super.initState();
    _loadUserInfo();
  }

  _loadUserInfo() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    _username = (prefs.getString('username') ?? "");
  }

  void _handleLogout() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        prefs.remove("username");
        Navigator.pushNamedAndRemoveUntil(
            context, '/login', ModalRoute.withName('/login'));
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Home"),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("Welcome " + _username + ". This is the home screen"),
            RaisedButton(
              onPressed: _handleLogout,
              child: Text("Logout"),
            )
          ],
        ));
  }
}

This screen has just two UI elements, a text and a logout button. We display a simple message Welcome username. This is the home screen. We retrieve the username entered in the login screen from persistent store using the sharedpreferences package. The logout button simple removes the username from the persistent store and navigates the user back to the login screen.

This was a very simple app to demonstrate the login flow of a flutter app. The entire source code can be found on github.