Yusuf Ahmed Software engineer, technical writer, vibes.

Building a responsive login page in Flutter

4 min read 1254

Building a responsive login page in Flutter

Flutter, as a cross-platform application framework, enables you to develop apps that look and feel great on any platform using a single codebase. With the release of Flutter 3.0, we now have Windows, macOS, and web completely stable on it.

This means that when developing an app for these platforms, we must take into account different device specifications and ensure that the appeal of our app is consistent and provides users with a seamless experience.

With this in mind, Flutter provides a variety of widgets and classes for creating responsive layouts in applications; including MediaQuery, LayoutBuilder, Expanded, Flexible, and AspectRatio.

In this article, we’ll be looking at how we can build a responsive login page using the two main approaches recommended by the Flutter team, which are the MediaQuery and LayoutBuilder classes.

Before proceeding, I recommend that you ensure you meet the following prerequisites to continue with this tutorial:

So with all that out of the way, let’s get started.

Table of Contents

The MediaQuery class

The MediaQuery class has a .of method that takes in a context and gives you access to the size (width/height) and orientation (portrait/landscape) of your current app.

Here is an example:

//...
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size; //getting the size property
    final orientation = MediaQuery.of(context).orientation; //getting the orientation
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Size of the screen: ${size.width}x${size.height}',
                style: const TextStyle(fontSize: 16)),
            const SizedBox(height: 20),
            Text('Orientation of the screen: $orientation',
                style: const TextStyle(fontSize: 16)),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
Using MediaQuery Class
Using MediaQuery class

The LayoutBuilder class

The LayoutBuilder class has a builder property that allows us to access a BoxConstraint object.

This object contains constraint information for a specific widget, which can be used to calculate the widget’s maxWidth and maxHeight. These values are crucial in adjusting our display based on the size constraints assigned to our widget.



Here’s an example:

//...
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('USING LAYOUTBUILDER'),
      ),
      body: Row(
        children: [
          Expanded(
            flex: 2,
            child: LayoutBuilder(builder: (context, constraints) {
              return Container(
                width: constraints.maxWidth,
                height: constraints.maxHeight,
                color: Colors.indigo.shade900,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Center(
                      child: Text(
                          'This is my max height: ${constraints.maxHeight}',
                          style: const TextStyle(
                              fontSize: 16, color: Colors.white)),
                    ),
                    const SizedBox(height: 20),
                    Center(
                      child: Text(
                          'This is my max width: ${constraints.maxWidth.toStringAsFixed(2)}',
                          style: const TextStyle(
                              fontSize: 16, color: Colors.white)),
                    ),
                  ],
                ),
              );
            }),
          ),
          Expanded(
            flex: 4,
            child: LayoutBuilder(builder: (context, constraints) {
              return Container(
                width: constraints.maxWidth,
                height: constraints.maxHeight,
                color: Colors.white,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('This is my max height: ${constraints.maxHeight}',
                        style: const TextStyle(fontSize: 16)),
                    const SizedBox(height: 20),
                    Text('This is my max width: ${constraints.maxWidth}',
                        style: const TextStyle(fontSize: 16)),
                  ],
                ),
              );
            }),
          ),
        ],
      ),
    );
  }
}
iPhone using layoutBuilder widger
Using LayoutBuilder widget

Now that we have a general idea of how they function, let’s use them to build a responsive login page that looks great on all platforms.

Project Setup

To begin with, we will be building a simple login screen similar to the images below:

Login page on mobile device
Mobile View
Login page on a desktop web browser
Web/Desktop View
Photo by Daniel on Figma Community

Now, let’s create a new Flutter project, and paste the following code into your main.dart file as the starting point of our app:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const LoginScreen(), //<--
    );
  }
}

Notice in the above code snippet that we have a LoginScreen widget; now let’s create the LoginScreen widget. This widget can house our LayoutBuilder class, as well as how we can return different screens based on the device size specifications.

class LoginScreen extends StatelessWidget {
  const LoginScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          if (constraints.maxWidth < 600) {
            return const LoginMobile();
          } else if (constraints.maxWidth > 600 && constraints.maxWidth < 900) {
            return const LoginTablet();
          } else {
            return const LoginDesktop();
          }
        },
      ),
    );
  }
}

From the code snippet above, notice how we set a width breakpoint and then used the constraint object derived from the LayoutBuilder widget to check if the device maxWidth value is greater or less than the width breakpoint — and return appropriate widgets as needed.

In our case, we return the mobile view if the maxWidth is less than 600, the tablet view if it is between 600 and 900, and the desktop/web view otherwise.


More great articles from LogRocket:


Mobile/tablet view

The LoginMobile widget basically has two TextFormField widgets that serve as our email and password fields, as well as a button to handle event submission, as shown in the code snippet below.

The mobile and tablet view essentially renders the same thing, so for the sake of brevity, I won’t be including the tablet view code.

class _LoginMobileState extends State<LoginMobile> {
  bool _isChecked = false;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(30),
          child: Center(
            child: SizedBox(
              width: 300,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Text(
                    'Welcome back',
                    style: GoogleFonts.inter(
                      fontSize: 17,
                      color: Colors.black,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Login to your account',
                    style: GoogleFonts.inter(
                      fontSize: 23,
                      color: Colors.black,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                  const SizedBox(height: 35),
                  TextFormField(
                   //...
                  ),
                  const SizedBox(height: 20),
                  TextFormField(
                   //...
                  ),
                  const SizedBox(height: 25),
                  Row(
                    //...
                  ),
                  const SizedBox(height: 30),
                  TextButton(
                   //...
                  ),
                  const SizedBox(height: 15),
                  TextButton(
                  //...
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Web/desktop view

For the web view, we’ll show a split screen with an image and our previously created login components. We do that by placing them side by side using a Row widget and then wrapping both sides with an Expanded widget to take up the remaining free space.

//...
class _LoginDesktopState extends State<LoginDesktop> {
  //...
  bool _isChecked = false;
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded( //<-- Expanded widget
          child: Image.asset(
             'assets/image 1.png', 
             fit: BoxFit.cover,
        )),
        Expanded( //<-- Expanded widget
          child: Container(
            constraints: const BoxConstraints(maxWidth: 21),
            padding: const EdgeInsets.symmetric(horizontal: 50),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Text(
                  'Welcome back',
                  style: GoogleFonts.inter(
                    fontSize: 17,
                    color: Colors.black,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  'Login to your account',
                  style: GoogleFonts.inter(
                    fontSize: 23,
                    color: Colors.black,
                    fontWeight: FontWeight.w700,
                  ),
                ),
                const SizedBox(height: 35),
                TextField(
                 //...
                ),
                const SizedBox(height: 20),
                TextField(
                 //...
                ),
                const SizedBox(height: 25),
                Row(
                  //...
                ),
                const SizedBox(height: 30),
                TextButton(
                  //...
                ),
                const SizedBox(height: 15),
                TextButton(
                 //...
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}
Web View
Web view:
Desktop (macOS) View
Desktop (macOS) view:

Changing Window Size

Conclusion

Achieving responsiveness in your app makes it look consistent and gives users a seamless experience, and we’ve seen a variety of approaches to tackling responsiveness. You can also look into responsive packages like responsivebuilder, responsivesizer, and responsive_framework.

Here’s a GitHub link to the sample project we made:

https://github.com/dev-tayy/responsiveloginscreen

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Yusuf Ahmed Software engineer, technical writer, vibes.

Leave a Reply