How to make a login screen with video background in flutter

April 24, 2020

What we’re building

This is a quick tutorial on how to design a login screen with a background video file, which I recently added to my app WaveSpy (iOS, Android).

This is what the screen will look like:

final

Project setup

Create the project

I’m assuming you have a flutter dev env set up. If not, you can use Flutter’s Getting Started.

Now let’s create a new project named flutter_login_video. In the terminal run:

flutter create --org com.learningsomethingnew.fluttervideo --project-name flutter_login_video flutter_login_video

It’s a good idea to do your first commit at this point

Add the video_player plugin

We’ll use the video_player plugin to display the video in the background. Add the following line to pubspec.yaml dependencies:

video_player: ^0.10.9+1

Add assets

Video asset

I’ve gotten good results with a 20 second 720p video - about 2 MB in size. So create such a video with your favorite video editor. Create a folder named assets in the main project folder, and put your mp4 file in a subfolder named videos (so it will be in assets/videos eventually).

Logo asset

For your app’s logo, create a square png with white foreground and transparent background. Put the image in assets/images.

Now add the assets to pubspec.yaml so flutter includes them in the generated app:

assets:
    - assets/videos/
    - assets/images/

Building the UI

We’ll create our new widget in sign_in.dart. Let’s look at some initializations before building the UI:

Init the video

We’ll initialize our video player in main.dart’s initState method:

_controller = VideoPlayerController.asset("assets/videos/waves.mp4");
_controller.initialize().then((_) {
  _controller.setLooping(true);
  Timer(Duration(milliseconds: 100), () {
    setState(() {
      _controller.play();
      _visible = true;
    });
  });
});

We’re initializing the controller with the asset method to load a local video. We’re setting the player to loops the video. Notice the video will start playing after 100 milliseconds, which is a small optimization to make app load faster. We need to also dispose of the controller in the dispose override:


void dispose() {
  super.dispose();
  if (_controller != null) {
    _controller.dispose();
    _controller = null;
  }
}

Force portrait mode

Forcing portrait or landscape mode should be used sparingly, but in this case I think it makes sense. In my opinion there’s no use-case where users really have to use landscape mode for logging in. This is a UI flow that’s usually done in portrait mode, and it helps if we know that we’re going to display the video in a portrait aspect ratio. We’ll use SystemChrome.setPreferredOrientations to lock the device into portrait mode. In initState add:

SystemChrome.setPreferredOrientations([
  DeviceOrientation.portraitUp,
]);

When you navigate to other screens that should support landscape (the rest of your app), just set:

SystemChrome.setPreferredOrientations([
  DeviceOrientation.landscapeRight,
  DeviceOrientation.landscapeLeft,
  DeviceOrientation.portraitUp,
  DeviceOrientation.portraitDown,
]);

The UI

Our UI will consist of a main centered Stack, which overlays three layers:

  1. The background video.
  2. A semi-transparent Container on top to give it a blue tint.
  3. Our content (text, buttons and app logo).

Let’s get into the code:


Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Stack(
        children: <Widget>[
          _getVideoBackground(),
          _getBackgroundColor(),
          _getContent(),
        ],
      ),
    ),
  );
}

I’ve split the main UI components into methods to make the code more readable. Let’s look into each one of the UI building methods.

Video background

This is basically the VideoPlayer widget (with the controller we initialized in initState), wrapped with an AnimatedOpacity widget to fade the video in.

_getVideoBackground() {
  return AnimatedOpacity(
    opacity: _visible ? 1.0 : 0.0,
    duration: Duration(milliseconds: 1000),
    child: VideoPlayer(_controller),
  );
}

Background overlay color

This is a simple Container with a color that has less than 255 alpha.

_getBackgroundColor() {
  return Container(
    color: Colors.blue.withAlpha(120),
  );
}

Foreground content

Now for the main content, which consists of the logo, title, subtitle, and buttons, all residing in one Column:

_getContent() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    mainAxisAlignment: MainAxisAlignment.start,
    children: <Widget>[
      SizedBox(
        height: 50.0,
      ),
      Image(
        image: AssetImage("assets/images/white-logo.png"),
        width: 150.0,
      ),
      Text(
        "WaveSpy",
        style: TextStyle(color: Colors.white, fontSize: 40),
      ),
      Container(
        margin: const EdgeInsets.only(left: 30.0, right: 30.0, top: 40.0),
        alignment: Alignment.center,
        child: Text(
          "View and share videos of current ocean conditions.",
          textAlign: TextAlign.center,
          style: TextStyle(color: Colors.white, fontSize: 20),
        ),
      ),
      Spacer(),
      ..._getLoginButtons()
    ],
  );
}

Notice the use of Spacer to make the buttons stick to the bottom of the Column. Notice also the use of the spread operator (added in sdk 2.2.2), that allows us to add the login buttons to the children list of Column in a functional manner.

Login buttons

Finally we’ll create the bottom login buttons:

_getLoginButtons() {
  return <Widget>[
    Container(
      margin: const EdgeInsets.only(left: 20, right: 20),
      width: double.infinity,
      child: FlatButton(
        color: Colors.white,
        padding: const EdgeInsets.only(top: 15.0, bottom: 15.0),
        child: const Text('Sign Up with Email'),
        onPressed: () async {},
      ),
    ),
    Container(
      margin: const EdgeInsets.only(top: 10, left: 20, right: 20),
      width: double.infinity,
      child: FlatButton(
        color: Colors.blueAccent,
        padding: const EdgeInsets.only(top: 15.0, bottom: 15.0),
        child: const Text(
          'Log back in',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () async {},
      ),
    ),
    Container(
      margin: const EdgeInsets.only(bottom: 16, top: 20, left: 20, right: 20),
      width: double.infinity,
      child: FlatButton(
        child: const Text(
          'Later...',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {},
      ),
    ),
  ];
}

Conclusion

That’s it! We have our login screen:

final

As always, full source can be found on GitHub. If you have any questions, please leave a comment!


Written by@Jonathan Perry
Fullstack dev - I like making products fast

GitHubMediumTwitter