Flutter – Authentication

In this post we will explore a common requirement - securing portions of your app behind a login screen.  In order to access certain pages in your app the user must first sign in.  After signing in, their user experience should be different - for example a home page with menu options.  Wouldn't it be nice you could easily build your app in such a way that once a user is signed in the app just builds them a different screen?

Well, that is exactly the approach we will take in this article.  In a nutshell, if the user is not yet authenticated, we will build and display a sign-in page.  But once the user has authenticated we will build and display their home page.  In order to do this, we will be using streams. Don't worry, this is really super-easy once you see the code.

Building an App using StreamBuilder

StreamBuilder is a pretty nifty way to build out the user interface for an app.  The way StreamBuilder works is really quite simple - listen for changes on a stream and based on what comes in from that stream, build the user interface as you see fit.  In the app we will build, we will have a stream that is listening for changes in the authentication state of the application.  As the state changes, the app will present different pages to the user.

Let's get started - create a new Flutter app using the command line or your favorite supported development environment.  If you need instructions on how to create a Flutter app, please see my blog post From Zero to App with Flutter

Since we are going to use a stream to build our app, it's a good idea to give some thought to what we will be receiving from the output of the stream. Since we are dealing with authentication in our app and a user that will be needing to sign in and sign out, we should expect to receive the following "states" from our stream:

  • initial - the user has not tried to authenticate or anything else just yet...
  • authenticated - the user is presently authenticated
  • failed - the user attempted authentication but it failed
  • signed out - the user has signed out

For now, let's define a new class to hold our expected states.  Create a new file under lib and name it authentication_state.dart.  Here is its contents:

class AuthenticationState {
  final bool authenticated;

  AuthenticationState.initial({this.authenticated = false});

  AuthenticationState.authenticated({this.authenticated = true});

  AuthenticationState.failed({this.authenticated = false});

  AuthenticationState.signedOut({this.authenticated = false});
}

The AuthenticationState class for now really only tracks one state - authenticated.  But you can see it is easily setup to also track other states such as if a user failed to sign in. One way that might prove useful is if you wanted to provide a message to the user that they're attempt to authenticate failed.

Our Sign In and Home Page

I mentioned out the outset of this article that we would be displaying different pages based on whether or not the user is signed in (authenticated).  Here they are:

Sign In Page

The Sign In Page page is simple.  It has it's own title and a button in the middle of the page with the text "Sign In".  Of course in a real world app you would likely need more on this page - such as a form to enter a username and password.  But let's keep it simple because our focus is on stream builders, not building forms.

 

Create a file under lib named signin_page.dart with the following contents:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:async';
import 'authentication_state.dart';

class SignInPage extends StatelessWidget {
  final StreamController<AuthenticationState> _streamController;

  SignInPage(this._streamController);

  signIn() async {
    _streamController.add(AuthenticationState.authenticated());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sign in')),
      body: Center(
        child: RaisedButton(
          child: Text('Sign in'),
          onPressed: signIn,
        ),
      ),
    );
  }
}

The code for the sign in page is really simple - the only logic here is the handler for when the sign-in button is pressed.  Within that handler, we add to our stream a new state: that the user has authenticated.  I'll show you later in this article how to use an authentication service to determine if the user can be authenticated or not but for now lets talk about the StreamController we see here.  The StreamController is simply a mechanism that provides the capability of sending data on a stream that others may listen for.  The StreamController is passed in when the sign-in page is created - don't worry I'll show you how this is done in just a bit...

Home Page

The home page is just as straight-forward with its own title and a button labeled "Sign Out".  For a real world app this is where you would consider having a nice tab or drawer menu.  But again, we are keeping our focus on the stream builder interaction...

 

Create a file under lib named home_page.dart with the following contents:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'authentication_state.dart';

class HomePage extends StatelessWidget {
  final StreamController<AuthenticationState> _streamController;

  const HomePage(this._streamController);

  signOut() {
    _streamController.add(AuthenticationState.signedOut());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Welcome')),
      body: Center(
        child: RaisedButton(
          child: Text('Sign Out'),
          onPressed: signOut,
        ),
      ),
    );
  }
}

The code for the home page is also super simple with a single pressed handler for the sign out button.  When pressed, a new state is added to the stream: that the user has signed out.  You'll notice we rely on a StreamController for our stream interaction here as well.

The Builder Page

So far, we have seen code for the various authenticates states as well as the sign-in and home pages.  But we have not seen how this is all put together.  To recap, our goal is to build a different user interface based on whether or not the user is authenticated or not.  To do that we need to do the following:

  1. Listen to the stream for state changes - such as if the user authenticated or signed out.
  2. Based on the state change, either present the Sign-In page or the Home page.

To accomplish this, we will create a landing page that will be the default page for the app.  Please open your main.dart file that was created along with your app and update its contents as follows:

import 'package:flutter/material.dart';
import 'package:flutter_login_example/builder_page.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Login Demo',
      theme: new ThemeData(
        primarySwatch: Colors.orange,
      ),
      home: new BuilderPage(),
    );
  }
}

Note the setting of home to a new instance of BuilderPage?  The builder page will hold the logic necessary to determine whether to present the sign-in or home page.  Let's create the builder page now by creating a new file under lib named builder_page.dart and add the following code:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_login_example/authentication_state.dart';
import 'package:flutter_login_example/signin_page.dart';
import 'home_page.dart';

class BuilderPage extends StatelessWidget {
  final StreamController<AuthenticationState> _streamController =
      new StreamController<AuthenticationState>();

  Widget buildUi(BuildContext context, AuthenticationState s) {
    if (s.authenticated) {
      return HomePage(_streamController);
    } else {
      return SignInPage(_streamController);
    }
  }

  @override
  Widget build(BuildContext context) {
    return new StreamBuilder<AuthenticationState>(
        stream: _streamController.stream,
        initialData: new AuthenticationState.initial(),
        builder: (BuildContext context,
            AsyncSnapshot<AuthenticationState> snapshot) {
          final state = snapshot.data;
          return buildUi(context, state);
        });
  }
}

Let's break this code down.  Firstly, we create a StreamController.  This stream controller is associated with the AuthenticationState type.  This allows us to look at the data coming in from the stream as any of our predefined authentication states - nice!

Next up, we have the buildUi method.  I like to break out the building of user interface widgets (especially when there is logic involved) to a separate method.  I just find it cleaner for me...  It is in this code block that we receive an AuthenticationState and if that state is authenticated we return the HomePage, otherwise we return the SignInPage.  But where does the AuthenticationState come from?  Remember in our sign-in and home pages where we were adding data representing AuthenticationStates to the stream? Well that data makes its way back here and we use it to decide how to build out the user interface.

To tie this all together, there is one more method to look at - build().  This method returns a new StreamBuilder that is associated with our stream controller.  Here we set the streams initial state as well as provide an implementation for the builder.  This implementation merely receives data from the stream when it arrives and calls the buildUi() method.  To put this altogether, our implementation is as such:

  1. Use a stream to listen for changes in authentication state.  When the state changes, present a different page to the user.
  2. Allow other pages/widgets in the app to send data on this stream to notify the builder page of changes in the authentication state.

Lets Run It!

Here is the app in all of its glory (albeit simple!):

Adding in Authentication via Http

If you go back and look at the SignInPage, the signIn() method simply authenticates the user as soon as the Sign In button is clicked.  In most cases you will call some sort of back-end service to authenticate a user.  Let's assume somewhere you have an Http RESTful service that you use to authenticate via a username and password.  We'll keep it simple and forgo some of the nuances such as keeping track of the user's session via something like a Json Web Token.  But in a real-app you would want to improve on what we do here.  Let's create a new file under lib called authentication_service.dart.  It's job will be to expose a method that calls an Http web service to authenticate the user.  Rather than setup a real web service, we'll just use Postman Echo to simulate things.  Here is the code for authentication_service.dart:

import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:async';

class AuthenticationService {

  Future<bool> authenticate(String username, String password) async {
    String url = "https://postman-echo.com/post?username=${username}&password=${password}";
    http.Response response = await http.post(url);
    return response.statusCode == HttpStatus.ok;
  }

}

Next, let's go back to the SignInPage class and declare an instance of the AuthenticationService class as well as modify the signIn meth0d as follows:

class SignInPage extends StatelessWidget {
  final StreamController<AuthenticationState> _streamController;
  AuthenticationService _authenticationService = new AuthenticationService();

  SignInPage(this._streamController);

  signIn() async {
    _streamController.add(AuthenticationState.authenticated());
    var result =
        await _authenticationService.authenticate("username", "password");
    if (result) {
      _streamController.add(AuthenticationState.authenticated());
    }
    else {
      _streamController.add(AuthenticationState.failed());
    }
  }
...

You'll also need to update the import statement at the top of the file to import the authentication_service.dart file you just created.  Now run the app again and you will notice it works the same as it did before - but in this case it calls an Http service to perform authentication.  Hopefully you can use this as a good starting point in your applications.

Sharing of Streams Between Widgets

We took a very simplistic approach in sharing the StreamController created in the BuilderPage with the SignInPage and the HomePage.  We just passed it in via the constructor.  This is fine for simple apps and should cover many cases.  But it does couple the pages together and doesn't scale perhaps as far as you may need it to.  There are other patterns and techniques that you can employ if you see a benefit in doing so.  Here are a few topics you might want to become familiar with if you are interested.  I may blog about these in the near future so be sure to check back here as well.

  • bloc pattern
  • scoped model

That's a wrap!

I hope you have found this article to be helpful!

Leave a Reply

Your email address will not be published. Required fields are marked *