Back

May 26, 2022

May 26, 2022

Build a Flutter Web App from Scratch: A Complete Guide

Build a Flutter web app from start to finish using Flutter and Dart. By the end of this tutorial, you'll have a movie catalog web app that allows you to sort films by genre, duration and more.

Build a Flutter Web App from Scratch: A Complete Guide.
Build a Flutter Web App from Scratch: A Complete Guide.
Build a Flutter Web App from Scratch: A Complete Guide.

Flutter is a development framework that allows developers to build applications across various platforms, namely Android, iOS, and the web. It’s one of the most-used mobile development frameworks. Flutter is built using the Dart programming language, so to use Flutter you need to know Dart. If you’re not already familiar with Dart, learning an entirely new language might seem like an extra chore. However, it’s easy to pick up and has some distinct advantages over other languages. It’s a single programming language that can develop apps for Android, iOS, and the web, which means you only need one codebase. Therefore, it’s much easier to maintain updates and add new features when you build a Flutter web app.

Flutter performs extremely well across different platforms because it applies each platform’s native code without the need for an intermediary layer to interpret it. It enjoys strong support from Google and has a vibrant community. The Flutter team and community also provide easy-to-follow documentation with straightforward step-by-step breakdowns, and written and video tutorials.

What Will You Learn from This Guide?

Let's build a Flutter app from scratch! If you’re new to Flutter, you’ll be introduced to the framework through setting up the dev environment, building a starter app, and running it in your browser. You’ll learn what widgets are and also how to build complex user interfaces from scratch using various types of widgets, as well as how to create your own widgets. You’ll learn how to pass data between widgets to create interactive and dynamic content and how to work with assets such as fonts and images (both local images and dynamically over the internet).

Finally, you’ll learn how to bundle and export your Flutter web app and get it ready to be hosted online.

For those already familiar with Flutter or who have completed production work with Flutter mobile, this tutorial will explain how you can get started with Flutter web and show you how you can get your application online. This tutorial is broken into several subsections to make it easier to follow, so feel free to skip to the relevant parts.

Getting Started: Setting Up Your Environment

Setting up Flutter is very simple; you can follow the installation procedure on the Flutter documentation page. After selecting your operating system, you’ll be directed to the relevant instructions. Fedora 35 Linux was used for this guide, so the following instructions demonstrate how to get set up on Linux:

  1. Download the SDK files using Git.

$ git clone https://github.com/flutter/flutter.git -b stable

2. Permanently add Flutter to your execution path so your system knows where to find and run Flutter-related commands or programs. You do this by adding the path to your f=Flutter directory to the PATH variable in your bash profile file ~/bash_profile.

$ echo “export PATH=$PATH:[path/to/flutter-directory]/bin” >> ~/.bash_profile

In the setup used for this tutorial, the above command will be as follows:

$ echo “export PATH=$PATH:$HOME/Android/flutter/bin” >> ~/.bash_profile

3. If this was done correctly, when you run $ which flutter in the terminal, it should print out the path to your Flutter installation. In the setup used for this tutorial, the path was as follows:

$ ~/Android/flutter/bin/flutter

4. Pre-downloading Flutter development binaries will make certain artifacts and binaries available offline, which may be needed during development:

$ flutter precache

5. Check that your installation and dependencies are properly set up by running the following:

$ flutter doctor

6. Enable Flutter for web. Earlier versions of Flutter (below version 2) are set up for Android and iOS mobile development by default and have to be enabled to build for the web. If you’re using an earlier version, you can do that by running the command below in your terminal; alternatively, you can just upgrade to the latest version (3.7 at the time of writing):

$ flutter enable web

7. Finally, to provide support for syntax highlighting and code completion for the Dart language and Flutter framework, you need to enable some plugins for your text editor or IDE of choice. The Flutter documentation officially supports and provides instructions for Android Studio, IntelliJ IDEs, Visual Studio Code, and Emacs Text Editor.

What Are You Building?

With your dev environment all set up, you’re ready to start Flutter app development. This guide explains how to build a movie catalog web application that shows a list of movies under specific categories, where the content changes in each category according to the data available. This application was chosen for this Flutter tutorial as it will help you learn most of the fundamentals.

The user interface (UI) takes inspiration from the design below by Dribble artist Zaini Achmad. Though it may seem complex for a beginner, building this application will allow you to appreciate the power of Flutter widgets and learn how to break down complex interfaces into simple and smaller units that are easier to build.

Please note, as this will not be a fully functioning web application, the data for the interface will be static and not all functionality will be covered. However, the tutorial will demonstrate how to design a Flutter UI that’s as close as possible to the sample artwork.

Movie catalog Flutter UI.

Building the App

Now that you’ve been introduced to Flutter and you have an idea about the app you’ll be building, it’s time to put together your Flutter web app and get it running. Each section will cover various tasks to explain and walk you through every aspect of building the application.

1. Create and Run the Initial Project

First, you need to create a starter project. Enter the terminal change directory to an appropriate location on your system where you can start a project and run the command below:

$ flutter create movie_catalogue

In the command above, you’re telling Flutter to create a new Flutter app with the name “movie_catalogue”. This will scaffold a basic Flutter starter app with the name “movie_catalogue” into a directory with the same name. You should have an output similar to this:

Creating a Flutter web app.

You can now change the directory to the project directory and run the basic app in your Chrome browser. Then, switch to the IDE of your choice (Android Studio for this tutorial). Open the project directory in your IDE to start writing some code.

$ cd movie_catalogue
$ flutter run -d chrome

You should have an output like this:

Initial starter app.

Before you get started, clean up the main.dart file in the root of the project directory to contain only the code below for now:

import 'package:flutter/material.dart';
void main() {
  runApp(const TheMovieCatalogue());
}
class TheMovieCatalogue extends StatelessWidget {
  const TheMovieCatalogue({Key? key}) : super(key: key);
  /// This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'The Movie Catalogue',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(),
    );
  }
}

In the above code, you’ve created a class called TheMovieCatalogue, which is the base of your app. It extends a Stateless widget, therefore making this class a widget (more to come on that in the “Building the Layout and User Interface” section below). It contains a build method that generates the final structure of the UI based on the various widgets described and their nested or lower-level widgets. In this case, you’re retrieving a MaterialApp widget from the Material library in Flutter (imported at the top of the file). This implements Google’s material design UI out of the box and gives you access to a wide range of widgets for building your user interface.

The void main() function you see is where the app gets called to start running when launched. Currently, if you run this Flutter web app, you’ll be presented with a blank page. Time to build your UI.

2. Data and Assets

Before you start to build a web app, you need to add some fonts and static images to your application, such as the background image file. You also need to add some data that provides basic functionality and “dynamic” content, as you won’t be developing an API server.

2.1 Adding the Assets

First, create a directory called assets in the root of the project directory. In the assets directory, create two directories, namely fonts and img. These will contain the font and image files that you’ll be using in your application. Copy and paste your background or other images into the respective directories. Feel free to use those from the complete tutorial code.

After that, declare the image and font files in the app’s configuration file pubspec.yaml located in the root of the project directory. Uncomment the respective lines and edit the file as follows:

# To add assets to your application, add an assets section, like the one below.
assets:
  - assets/img/
fonts:
  - family: Gothic A1
fonts:
  - asset: assets/fonts/GothicA1-Regular.ttf
  - asset: assets/fonts/GothicA1-Medium.ttf
  - asset: assets/fonts/GothicA1-Light.ttf
  - asset: assets/fonts/GothicA1-Bold.ttf

2.2 Movie Data

The entire data file can be obtained from the project repository in this data.dart file. However, due to the extensive nature of the data, how it’s declared and will be used in your application is only briefly explained here.

The movie data — obtained for free from TMDB—is structured in a standard JSON format. You can register for a free developer account to get access to their API. The movies are arranged in four list sets for the four categories in the application’s main navigation, namely, “New Releases,” “Most Popular,” “Recommended,” and “Top Chart.” Each set contains twenty items of movie details. Our data also contains a list of genres for the movies.

In the file linked above, you have six constant variables declared and initialized:

const String pImageBase = 'https://image.tmdb.org/t/p/w342';
const String bImageBase = 'https://image.tmdb.org/t/p/w300';
const List genres = [];const List> newReleases = [];const List> mostPopular = [];const List> recommended = [];const List> topChart = [];
  • pImageBase: a string for the base url for the poster image of each movie.

  • bImageBase: the string value for the base url for the backdrop images of each movie.

  • genres: a list of movie genres with their ID and names.

  • newReleases, mostPopular, recommended, and topChart are all lists of movies for the menu items “New Releases,” “Most Popular,” “Recommended,” and “Top Chart,” respectively.

Note: In Dart, a JSON object is of type Map because the keys of an object can be numeric or string values. However, since you’re sure that the keys of your objects are strings, you specify the map as Map. Therefore, for a list of JSON objects that is a list of maps, you type List>.

Each object has a structure as below:

{
  "video": false,
  "vote_average": 7.9,
  "id": 438631,
  "overview": "Paul Atreides, a brilliant and gifted young man born into a great destiny beyond his understanding, must travel to the most dangerous planet in the universe to ensure the future of his family and his people. As malevolent forces explode into conflict over the planet's exclusive supply of the most precious resource in existence-a commodity capable of unlocking humanity's greatest potential-only those who can conquer their fear will survive.",
  "release_date": "2021–09–15",
  "adult": false,
  "backdrop_path": "/jYEW5xZkZk2WTrdbMGAPFuBqbDc.jpg",
  "vote_count": 6520,
  "genre_ids": [
    878,
    12
  ],
  "title": "Dune",
  "original_language": "en",
  "original_title": "Dune",
  "poster_path": "/d5NXSklXo0qyIYkgV94XAgMIckC.jpg",
  "popularity": 627.437,
  "media_type": "movie"
}

With your data in place, you can now proceed.

3. Building the Layout and User Interface

Flutter UIs are built on blocks of widgets. Each and every item consists of one or more widgets just like UIs in standard web development are built using blocks of HTML elements. Each widget provides you with options to describe what it should look like or how it should behave: Flutter has a widget for almost anything you can imagine.

This section will cover how to get started using widgets to build the layout and UI for your Flutter web app.

To make this simple, you can break down the design into various sections as depicted in the diagram below, identify the units within them to build them individually, and finally bring them all together.

UI Layout components of the Flutter web app.

3.1 Creating the Overall App Layout

You need to create a main widget that will provide the overall structure in which the various widgets for the individual sections will be placed. Below is a skeleton and tree of the layout that this tutorial will help you achieve. It lists the main widgets that are going to be used to compose the various sections.

However, some of these widgets will be wrapped in other widgets not shown in the diagram or tree. They are mainly to introduce some level of control or specific visual behaviors and effects, but will not affect the main structure.

Flutter framework layout diagram and tree.

To get started, create a file called layout.dart inside the lib directory of the project root and edit its content as follows:

import 'dart:ui';
import 'package:flutter/material.dart';
/// a. creating StatefulWidget
class AppLayout extends StatefulWidget{
  const AppLayout({Key? key}) : super(key: key);
  @override
  State createState() {
    return _AppLayoutState() ;
  }
}
/// b. Creating state for stateful widget
class _AppLayoutState extends State{
  @override
  Widget build(BuildContext context) {
/// returning a container widget
    return Container(
/// c. Setting a background image for entire layout
      decoration: const BoxDecoration(
        image: DecorationImage(
          image: AssetImage("assets/img/bg.jpg"),
          fit: BoxFit.cover,
        ),
      ),
/// d. Using Backdrop filter to blur the underlying image for the background
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
/// e. Creating the parent row
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
/// f. First Column for Left pane Section
            Container(
              width: 300,
              child: Column(),
              color: Colors.indigo.withOpacity(0.95),
            ),
/// g. Second column for Headers and Main Pane sections
            Expanded(
                child: Column(
                  children: [
/// h. Main Header section
                    Container(
                      height: 120,
                      color: Colors.indigo.withOpacity(0.80),
                      child: Row(),
                    ),
/// filter section
                    Container(
                      height: 120,
                      color: Colors.deepPurple.withOpacity(0.60),
                      child: Row(),
                    ),
/// i. Main Pane section
                    const Expanded(
                        child: Center(
                          child: Text("Hellooooo World"),
                        )
                    )
                  ],
                )
            )
          ],
        ),
      ),
    );
  }
}

Here’s what’s going on in the code snippet above:

1.  You create a StatefulWidget called “AppLayout” that’ll be used to contain and coordinate all other child widgets. You use a StatefulWidget because you’ll be handling and manipulating data for dynamic content based on specific events or actions. This can only be done in a widget with a state.

2. As explained above, you need to create another class _AppLayoutState to hold the State of our StatefulWidget as seen above. In the build method of the _AppLayoutState, you return a Container() widget. This container allows you to define a background image for the entire application by passing BoxDecoration as an argument to the decoration key or parameter.

3. The BoxDecoration widget is used to set the background image by specifying a DecorationImage widget as an argument to the image parameter. This DecorationImage also accepts an ImageProvider to provide the actual image to be displayed. Here, an AssestImage widget is used as an ImageProvider. It accepts a relative path to the image file to be displayed. The fit parameter is used to determine the behavior of the image across the entire container widget. In this case, you want it to cover the entire container using the Boxfit.cover argument.

4. Notice the child of the Container widget is a BackDropFilter widget. This BackDropFilter widget allows you to apply image filters on its parent widgets. This means it actually has no effect on the child widgets; only the Container with the image background is affected. Here, you’re using it to blur the background image.

5. This line creates the parent Row widget, which allows you to arrange elements horizontally. You align all the contents of the row to the center.

6.  For the children, just as you have in the tree, you define the first column for the Left Pane section, but the column widget does not allow you to specify a width for it. Depending on the size required by its children widgets, it uses the space made available to it by the parent widget. Therefore, you wrap it with a container widget where you define a width of 300. You also specify the color indigo to make that section visible when you run your app.

7. Here you define a Column widget inside an Expanded widget to allow the column’s children to utilize the screen space left after it assigns 300 pixels to the container for the Left Pane. The column contains three children: the first for the main header with the search bar, the second for the sort and filter section, and the third for the main pane.

8. For the header and filter sections, you have two Rows created each within a Container of height 120 and assigned colors to make them visible when you run the app.

9. For the Main Pane section, you use another expanded widget so it can expand to fill and utilize the screen space left after assigning the various heights to the header and filter sections.

Now run the app in your browser — using either your IDE controls or the terminal — using this command:

$ flutter run -d chrome

You should have an output similar to the one below.

App layout UI.

3.2 The Left Pane

Create a directory inside the lib directory of your project root and name it widgets. Create another directory there called leftpane. This directory will contain all the code and custom widgets related to the left pane.

Next, you’ll create the following files —  main_nav_item.dart, sub_nav_item.dart, and left_pane_widget.dart— inside the leftpane directory, and then follow the instructions below for creating the content within each.

3.2.1 For the main_nav_item.dart File

Create main_nav_item.dart and within it create a custom widget named MainNavItem. As the name implies, this is where you define how each menu item in the main navigation should look and work. Edit the file to contain the code below:

import 'package:flutter/material.dart';
class MainNavItem extends StatelessWidget{
/// a. definition of variables
  final String title;
  final bool isSelected;
  final VoidCallback action;
  final IconData? icon;
  const MainNavItem(this.title, this.icon, this.isSelected, this.action, {Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
/// b. returning a container
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 40),
/// c. making the item clickable
      child: MaterialButton(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        color: isSelected ? Colors.deepPurple.withOpacity(0.15) : null,
        onPressed: action,
/// d. Row for text and icon
        child: Row(
          mainAxisSize: MainAxisSize.max,
          children: [
            Icon(icon, color: Colors.white, size: 20, ),
            const SizedBox(width: 10,),
            Text(title, style: const TextStyle(fontSize: 20,color: Colors.white, ),),
          ],
        ),
      ),
    );
  }
}

In the above code:

1.  You declare a set of variables to help define the title, the icon, and the selected state of your menu items.

2.  For the build method returning the widget, you first define a Container with padding horizontally for the space before and after each menu item.

3. From the Material library, the MaterialButton widget is used to add a clickable button widget within which each menu item title and icon will be wrapped. You then use the isSelected variable to determine the color of the button. If true, highlight it with a purple color; if false, leave it. The onPressed parameter is used to define a function that’s called when the menu item (which is actually a button) is clicked or pressed. For this, you’ll pass action to the menu item. The value of the action will be a function passed from the parent widget of each menu item during its creation.

4. For the child of the MaterialButton widget, you’ll define a row widget that contains an Icon and a Text widget with a SizedBox widget in between them. SizedBox is used to define the space between the two widgets on the same row. Considering all the menu items have the same format or styling (where icons have the same color and size), you define them as constants and only pass an IconData as an argument to the Icon widget constructor. The same applies to the Text widget. You define a style with constant values white and font size 20, but for the text value, you pass on the values from your constructor as an argument to the Text widget.

3.2.2 For the sub_nav_item.dart File

Now create a file called sub_nav_item.dart in the leftpane directory, and within that, create custom widget SubNavItem for each menu item in the lower navigation section. This widget will define how each item should look and work. Edit the file to contain the following code:

class SubNavItem extends StatelessWidget{
/// declaration of variables with null safety
  final String title;
  final bool isSelected;
  final VoidCallback action;
  final IconData? icon1;
  final IconData? Icon2; //parameter for second icon
  final double? textSize; //parameter for text size
  const SubNavItem(this.title, this.textSize, this.icon1,this.icon2, this.isSelected, this.action, {Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
/// returning a container
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 40),
/// making item clickable
      child: MaterialButton(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        color: isSelected ? Colors.deepPurple.withOpacity(0.15) : null,
        onPressed: action,
        child: Row(
          mainAxisSize: MainAxisSize.max,
          children: [
            Icon(icon1, color: Colors.white, size: 20, ),
            const SizedBox(width: 10,),
            Text(title, style: TextStyle(fontSize: textSize ?? 18,color: Colors.white, ),),
            const SizedBox(width: 20,),
            Icon(icon2, color: Colors.white, size: 20, ),
          ],
        ),
      ),
    );
  }
}

As you’ve probably noticed, the code above is very similar to that of the MainNavItem widget you previously created. The differences between the two are explained below:

  • You can see from the artwork that the first menu item in the sub-navigation menu has an icon before the title and another icon after the title. The font size is also bigger than the subsequent menu items. Therefore, in the SubNavItem widget, you have added two more variables, one for the end icon and the second for the font size.

  • The variables are “Null Safe.” Simply put, they can be initialized to null, so that for the same widget you can decide to provide only the title without the icons and it will be displayed as one of the smaller menu items. If you do provide icons, they’ll appear in their respective positions.

  • Also, the default text size is 18, which is smaller than the MainNavItem. Therefore, for a bigger menu item size like the first one, you can specify the size parameter when creating the widget.

3.2.3 For the left_pane_widget.dart File

In this file, you define the structure of the left pane, which brings together the previously defined widgets for this pane. It will be a StatelessWidget and consist of a Column widget with one Container widget for the logo and two Column widgets, one for the upper or main navigation and the other for the lower sub-navigation. Edit the file with the following code:

import 'package:flutter/material.dart';
import 'package:movie_catalogue/widgets/leftpane/main_nav_item.dart';
import 'package:movie_catalogue/widgets/leftpane/sub_nav.dart';
class LeftPane extends StatelessWidget{
/// declaration of variables
  final int selected;
  final Function mainNavAction;
  const LeftPane({Key? key, required this.selected, required this.mainNavAction}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.max,
      children: [
/// the logo
        Container(
          height: 170,
          decoration: const BoxDecoration(
              border: Border(bottom: BorderSide(color: Colors.white, width: 4)),
              image: DecorationImage(image: AssetImage("assets/img/logo.png"),fit: BoxFit.cover)
          ),
        ),
//. The Upper or Main Navigation Menu
        Expanded(child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const SizedBox(height: 50,),
            MainNavItem("New Releases", Icons.rocket_launch_outlined, false ,(){}),
            MainNavItem("Most Popular", Icons.emoji_events_outlined, false, (){}),
            MainNavItem("Recommended", Icons.verified_outlined, false, (){}),
            MainNavItem("Top Chart", Icons.diamond_outlined, true, (){}),
          ],
        )),
/// Sub Navigation Menu
        Expanded(
            child: Column(
                children: [
                  SubNavItem("My Collection", 20, Icons.stop_circle_rounded, Icons.arrow_drop_down, false, (){}),
                  SubNavItem("Bookmark",null, null, null, false, (){}),
                  SubNavItem("History", null,null, null, false, (){}),
                  SubNavItem("Subscriptions", null,null, null, false, (){}),
                ]
            )
        ),
      ],
    );
  }
}

In the code above, as pointed out earlier, you add the logo, the main navigation menu, and the sub-navigation menu.

  • The logo: The first child widget of the Column widget is a Container defined with a height of 170 and a decoration argument defining an AssetImage with the logo as background. It also defines a border argument with a bottom border of white color and a width of 4.

  • The Upper or Main Navigation Menu: You then define a Column setting the CrossAxisAlignment to the center, so that the children of the column will be aligned vertically in the middle as you see in the artwork. The children of the Column include the size box at the top to introduce some space between the logo and the first menu item.

  • Next, you have the four main navigation items using the widgets created earlier in this section. As you can see, you first pass in the title of the menu item as a string value, then you pass on the respective icon, a Boolean value to tell the widget if it’s selected or not, and an empty anonymous function. This creates the menu items that will be clickable but won’t do anything if you click. Don’t worry about this for now.

  • Sub Navigation Menu: Next is another Column widget just as you did for the main navigation pane. Its children are four SubNavItem widgets. Just as you defined, this widget takes a String for the title of the menu item, a double for size and then two icons, (the first one for the icon before the title and the second one for the icon after the title), then a Boolean value and a function.

You can see that, with the exception of the very first item on this menu, you don’t provide the icons and font size for the subsequent items even though you’re using the same widget. This will show when you run the application, as the latter three menu items will be smaller using the default font size, 18.

To see these changes when you run the app, create the LeftPane widget in the appropriate section of the AppLayout widget you created earlier. Replace the child of the Container with the width of 300 from Column to LeftPane as seen below.

Change this:

Container(
width: 300,
child: Column(), /// replace this
color: Colors.indigo.withOpacity(0.95),
)

to this:

Container(
width: 300,
child: LeftPane(mainNavAction: (){}, selected: 0,), /// with this
color: Colors.indigo.withOpacity(0.95),
)

Now run the app to see what you have. Your output should be similar to the image below. You’ll notice the menu items are clickable but nothing happens. You’ll work on that later.

Completed Left Pane UI.

3.3 The Main Header

This section consists of two major components: the Profile Section and the Search Bar. Start by creating a directory named mainheader inside the widgets directory and create the following three files to provide the content for the components.

3.3.1 The profile_section File

In this file, you’ll create a StatelessWidget called “ProfileSection” that returns a Row containing three main widgets for the user’s name, the profile thumbnail, and a settings icon.

import 'package:flutter/material.dart';
class ProfileSection extends StatelessWidget{
  const ProfileSection({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Row(
      children: const [
/// Text widget for user's name
        Text("Rexford Nyarko", style: TextStyle(color: Colors.white, fontSize: 18),),
        SizedBox(width: 20,),
/// Circular profile thumbnail
        CircleAvatar(backgroundImage: AssetImage("assets/img/profile.thumbnail.jpeg"), radius: 35,),
        SizedBox(width: 15,),
/// setting icon
        Icon(Icons.settings, color: Colors.white,),
        SizedBox(width: 40,),
      ],
    );
  }
}

As you can see, this is a very simple widget.

  • You have a Row widget containing a Text, a CircleAvatar, and an Icon widget separated by SizedBox widgets.

  • The Text widget is used for the user’s name. This is just a constant string value and should ideally be loaded from an API backend.

  • The CircleAvatar is a widget that accepts an image and formats it within a circle. In this case, you’re providing an AssetImage widget with the path to your profile thumbnail photo file. This could also be a NetworkImage widget that would load the image from a server dynamically if you’re getting your data via a backend API as stated earlier. You can determine the size of this widget by specifying the radius as we’ve done here with the value 35.

  • The next is the Icon widget, where you specify a white settings icon. Ideally, it should be able to respond to clicks and, therefore, should have been wrapped with a MaterialButton as was done with the menu items. But the aim is mainly to achieve the UI represented in the artwork.

  • The SizedBox widgets you see were used to define spaces in between these three widgets.

3.3.2 The search_bar.dart File

The Search Bar consists of two main visual components, the search icon and the search text input box. Instead of creating two separate widgets for this, just one is sufficient. Take a look at the code below for the SearchBar widget:

dart
import 'package:flutter/material.dart';
class SearchBar extends StatelessWidget{
  const SearchBar({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
/// returning Flexible widget
    return const Flexible(
/// Text input box
        child: TextField(
          decoration: InputDecoration(
/// Search Icon
            prefixIcon: Padding(
              padding: EdgeInsets.symmetric(horizontal: 30),
              child: Icon(Icons.search, color: Colors.white60, size: 30,),
            ),
/// Hint text in search box
            hintText: 'Search By Title, Genre and Year',
            hintStyle: TextStyle(color: Colors.white60, fontSize: 20),
/// Remove borders
            border: InputBorder.none,
            contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 30),
          ),
/// Cursor color and text style
          cursorColor: Colors.white60,
          style: TextStyle(color: Colors.white60, fontSize: 20, ),
          cursorHeight: 25,
        )
    );
  }
}

As you can see above, you’re returning a Flexible widget so that the search area will fill the rest of the screen space after assigning the required space for the ProfileSection widget.

  • Text input box: You define a TextField widget, which provides an input box that can be used to decorate and style according to one’s needs.

  • Search icon: As part of the decoration for the TextField widget, you can define a prefixIcon, which is essentially an icon that’s shown before the input box. You specify the Icon widget with the search icon and wrap them inside a Padding widget to provide some spacing around the icon as seen in the artwork.

  • Hint text search in text box: You use the hintText parameter to provide a text on what can be searched in the search input field and you also define the styling for that text.

  • Remove borders: By default, the TextField widget has some borders, and here, you specify that this TextField widget should have no borders.

  • Text color and style: You specify the color for the cursor and the style for the text when something is typed in the search box.

3.3.3 The main_header.dart File

In this file, you have a simple structure where the SearchBar and ProfileSection widgets are combined into a Row widget:

import 'package:flutter/material.dart';
import 'package:movie_catalogue/widgets/mainheader/profile_section.dart';
import 'package:movie_catalogue/widgets/mainheader/search_bar.dart';
class MainHeader extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Row(
      children: const [
        SearchBar(),
        ProfileSection()
      ],
    );
  }
}

Once again, update the AppLayout widget to include the MainHeader widget instead of the existing Row widget. You should edit the layout.dart file as follows:

/// Main Header with search and profile
Container(
height: 120,
color: Colors.indigo.withOpacity(0.80),
child: MainHeader(),
),

Running the app should now give you the following output.

Completed Main Header with search and profile.

3.4 The Sub Header

This section has two major components. The first is the Sort Section and the second is the View Controls. Create a directory named subheader inside the widgets directory. You’ll then create the three files — sort_control.dart, view_controls.dart, and sub_header.dart — and define the contents for each file as per the specific instructions below.

3.4.1 The sort_control.dart File

In this file, you define the controls for the sorting and filtering section. Similar to the search bar section of the main header, you return a row wrapped inside a flexible widget.

import 'package:flutter/material.dart';
class SortControl extends StatelessWidget{
  const SortControl({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Flexible(
        child: Row(
          children: [
/// First sized box for space
            const SizedBox(width: 100,),
/// Sort by label
            const Text("Sort by ", style: TextStyle(color: Colors.white60,fontSize: 18),),
            const SizedBox(width: 20,),
/// Filter options
            DropdownButton(
              underline: Container(),
              style: const TextStyle(color: Colors.white,),
              iconEnabledColor: Colors.white,
              items: [
                DropdownMenuItem(
                  onTap: (){},
                  child: const Padding( padding: EdgeInsets.all(8.0), child: Text("Duration"),),
                ),
              ],
              onChanged: (selected){},
              autofocus: true,
            )
          ],
        )
    );
  }
}

The row contains the following children:

  • First, a SizedBox widget is used to create space before the rest of the widgets.

  • Next, you have a Text widget that you use as a label titled “Sort by.”

  • Then there is another SizedBox that’s used to create space between the Text and the next widget.

  • Finally, you add a DropdownButton widget for the filter options. This widget allows you to specify a number of dropdown menu items using the items parameter of that widget as seen above. Here you add only one DropdownMenuItem, named “duration.” It provides other parameters to define actions that should happen when any of the menu items is selected. But for now, you leave it with an empty anonymous function.

3.4.2 The view_controls.dart File

This widget is very simple; it essentially contains two icons, one of which is for a list view and the other is for a grid view. These are separated with some spacing using the SizedBox. For the purposes of this Flutter web tutorial, you’re only trying to mimic the UI in the artwork, so these are mainly icons, but ideally those icons should be wrapped in Button or GestureDetector widgets and have defined actions for when they are clicked.

import 'package:flutter/material.dart';
class SortControl extends StatelessWidget{
  const SortControl({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Flexible(
        child: Row(
          children: [
/// First sized box for space
            const SizedBox(width: 100,),
/// Sort by label
            const Text("Sort by ", style: TextStyle(color: Colors.white60,fontSize: 18),),
            const SizedBox(width: 20,),
/// Filter options
            DropdownButton(
              underline: Container(),
              style: const TextStyle(color: Colors.white,),
              iconEnabledColor: Colors.white,
              items: [
                DropdownMenuItem(
                  onTap: (){},
                  child: const Padding( padding: EdgeInsets.all(8.0), child: Text("Duration"),),
                ),
              ],
              onChanged: (selected){},
              autofocus: true,
            )
          ],
        )
    );
  }
}

3.4.3 The sub_header.dart File

This file is where you bring together the SortControl widget and ViewControls widgets in a Row widget to represent the sub header widget:

import 'package:flutter/material.dart';
import 'package:movie_catalogue/widgets/subheader/sort_control.dart';
import 'package:movie_catalogue/widgets/subheader/view_control.dart';
class SubHeader extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Row(
        children: const [
          SortControl(),
          ViewControls()
        ]
    );
  }
}

Update the AppLayout widget to now include the SubHeader widget instead of the existing Row widget. Edit the layout.dart file as follows:

/// Sub header with sort and filter
Container(
height: 120,
color: Colors.deepPurple.withOpacity(0.60),
child: SubHeader(),
),

Running the app should now give you an output that looks like the image below.

Completed Sub header UI.

3.5 The Main Pane

This widget is the part of the application to which most attention will be paid by users, as it contains the main content. This part of the app takes the data provided and creates a grid of scrollable tile items. The contents of the file are as follows:

import 'package:flutter/material.dart';
import '../data.dart';
class MainPane extends StatelessWidget {
  final List> data;
  const MainPane({Key? key, required this.data}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 20),
        itemCount: data.length,
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
            crossAxisSpacing: 50,
            mainAxisSpacing: 20,
            maxCrossAxisExtent: 300,
            childAspectRatio: 2.8/5
        ),
        itemBuilder: (BuildContext context, int index){
          return Column(
              children:[
                Flexible(
                  flex: 1,
                  fit: FlexFit.loose,
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(10),
                    child: GridTile(
                      child: Image(
                        image:NetworkImage(pImageBase + data[index]["poster_path"]),
                        fit: BoxFit.fill,
                      ),
                      footer: Container(
                        alignment: Alignment.centerRight,
                        margin: const EdgeInsets.all(12),
                        child: ClipRRect(
                          borderRadius: BorderRadius.circular(3),
                          child: Container(
                            padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 3),
                            color: Colors.yellowAccent,
                            child: Text("\u{2605} " + data[index]["vote_average"].toString(),
                              style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 17, color: Colors.black),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                Container(
                    alignment: Alignment.centerLeft,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text( data[index]["original_title"],
                          style: const TextStyle(fontSize: 17, color: Colors.white),
                        ),
                        Text(getGenre(data[index]["genre_ids"]),
                          style: const TextStyle(fontSize: 15, color: Colors.white60),
                        ),
                      ],
                    )
                ),
              ]
          );
        }
    );
  }
}
  • First, you declare a variable to hold the list of movie data.

  • Then, you define a constructor that allows you to initialize the data variable. This means when creating or instantiating this widget, you need to provide a list of maps with the movie data that will be used to build the GridView of movies.

  • In the build method, you return a GridView.builder widget. This widget allows you to build a GridView by specifying the number of parameters and also describing how each tile item in the grid will look.

  • Add a padding of 100 horizontally and 20 vertically to create some space from the edges of the Main Pane area.

  • The itemCount parameter is provided with the value of the length or number of items in the list of data that will be received from the constructor.

  • The gridDelegate parameter is provided a SliverGridDelegate widget. According to Flutter’s documentation, this widget is used to control the layout of tiles in a grid. It uses the various constraints provided to compute the layout of the tiles in the grid.

  • In this case you are providing a SliverGridDelegateWithMaxCrossAxisExtent widget. This widget creates a layout with the maximum number of items that can fit horizontally in the grid, meaning that the size of each grid item and the number of items shown per row in the grid will vary depending on the screen size of the device. This level of responsiveness exists in this widget by design, but is not present in other elements of the implemented UI, as the corresponding widgets don’t include responsiveness by design and adding such functionality is beyond the scope of this tutorial.

  • For the gridDelegate parameter, you want to specify some spacing between the grid items, both horizontally and vertically. That’s done with the values of 50 and 20, respectively, to crossAxisSpacing and mainAxisSpacing.

  • The maximum width of each item is defined by the value of 300 assigned to the maxCrossAxisExtent parameter.

  • And finally an aspect ratio of 2.8/5 is specified to determine the ratio of width to height of each item.

  • Building individual grid items with itemBuilder: The ItemBuilder parameter is used to define various widgets and describes how each tile should look. This parameter takes an anonymous function that accepts a BuildContext and an index (of the current item in the data list) and returns a widget. Here, you return a Column widget containing a Flexible widget and a Container widget.

  • Creating rounded corners with ClipRRect: The Flexible widget contains a ClipRRect widget. This widget is used to round the corners of its child widget. This allows each item in the grid to have rounded corners. The roundness of the corner is defined by passing a circular border radius of 10 to the borderRadius parameter of this widget.

  • The GridTile widget is assigned an Image widget as a child and a Container widget as a footer. As the tile images are going to be dynamically loaded over the network, a NetworkImage is used as a provider to the Image widget. The URL string to the image file is specified by concatenating the base URL from the value of pImageBase and the image filename for each tile to get the complete URL string.

  • The footer of the tile is assigned a Container that is used to define the movie rating on the lower right side of the tile.

  • Defining the movie title and genre: Next, the movie title and the genre are defined. All of these are placed in a Column widget so you can have the title at the top and the genre beneath it. The outer Container widget is used to align the contents to the center left using the alignment parameter.

  • Both the title and the genre are defined using Text widgets with the string values obtained from the data items provided.

  • For the movie genre, you create a function at the bottom of the class named getGenre that uses the IDs of the genre provided for each movie to obtain the title of the genre from the list of genres in the genre data set. This function can be seen below:

/// dynamically getting genre name with IDs
String getGenre( List gIndex){
  String genre = "";
  gIndex.asMap().forEach((index, value) {
    var g = genres.firstWhere((element) => element["id"] == value, orElse: () => {});
    if (index < 2 && g.isNotEmpty){
      genre += g["name"]+" ";
    }
  });
  return genre;
}

Finally, you need to add the MainPane widget to the AppLayout and also provide some data when instantiating the MainPane widget. To do that, you import your data.dart file (that was created at the beginning of the tutorial) into the layout.dart file by adding the following line to the top of the file:

import ‘package:movie_catalogue/data.dart’;

Next, you replace the “Hellooooo World” `Text` widget with the MainPane and pass the topChart data variable from the data.dart file as an argument to the data parameter as follows:

Change this block:

/// Main Pane
const Expanded(
  child: Center(
    child: Text("Hellooooo World"),
  ),
)

to this:

dart
/// Main Pane
Expanded(
  child: Center(
    child: MainPane(data: topChart) //here
  ),
)

Running the app should now give you an output similar to the following image.

Completed Main Pane Flutter UI.

4. Adding Basic Functionality

As stated earlier in this tutorial, the goal is to achieve basic functionality in our Flutter web app. In this case, making the main navigation on the left pane functional. In other words, clicking on each item will do two things:

1. Change the data for the GridView in the MainPane widget to the respective data for the selected menu item and update the UI with the new data.

2. Toggle the isSelected state of the menu item to show it’s currently selected.

4.1 Basic State Management

To be able to get the functionality needed, you have to understand basic state management in Flutter. In the layout.dart file, the AppLayout widget was created as a stateful widget mainly because this is where all the other parts of the application come together.

First, you introduce two new variables. One is a list of maps with the movies and the other is an integer:

  • The integer will be used to hold the current page.

  • The map will be used to hold the current data to be shown.

These should be placed right before the build method as follows:


class _AppLayoutState extends State{  List> data = topChart;
  int _currentPage = 4;
  @override
  Widget build(BuildContext context) {
  

Second, you have to add a method that’ll be used to change the values of the variables you just added each time a menu item is clicked. This method will be passed down to the LeftPane widget. Therefore, it will accept two arguments, an integer to set the current page and the list of maps of movies to change the data. The snippet below should be placed before the end of the state class definition’s closing braces:

void menuAction(int page, List> data){
  setState(() {
    _currentPage = page;
    this.data = data;
  });
}

In Flutter, every time you need to update the UI with a new set of values, you call setState(). This will ensure that all variables with updated values are used to reconstruct or refresh the widget trees in order to reflect the current data. So every time a menu item is clicked, setState() will be called in your function and used to update the page number and the data for the main pane.

Now, you pass the menuAction() method you just created and the _currentPage variable as arguments to the left pane widget as follows:

/// left pane
Container(
  width: 300,
  child: LeftPane(
    mainNavAction: menuAction, 
    selected: _currentPage,
  ),
  color: const Color(0xFF253089).withOpacity(0.85),
),

You also need to pass the data variable as an argument to the MainPane widget as follows:

/// Main Pane
Expanded(
  child: Center(
    child: MainPane(data: data,)
  ),
)

4.2 Toggling the Selected and Setting Click Actions

Now in the LeftPane widget, you need to pass your action function to each menu item. There’s also a need to dynamically toggle the selected state of each menu item using the selected value passed to the parent widget. This can be done by simply modifying each menu item call as follows:


children: [
  const SizedBox(
    height: 50,
  ),
  MainNavItem(
    'New Releases', 
    Icons.rocket_launch_outlined, 
    selected == 1 ,
    () => mainNavAction(1, newReleases),
  ),
  MainNavItem(
    'Most Popular', 
    Icons.emoji_events_outlined, 
    selected == 2, 
    () => mainNavAction(2, mostPopular),
  ),
  MainNavItem(
    'Recommended', 
    Icons.verified_outlined, 
    selected == 3, 
    () => mainNavAction(3, recommended),
  ),
  MainNavItem(
    'Top Chart', 
    Icons.diamond_outlined, 
    selected == 4, 
    () => mainNavAction(4, topChart),
  ),
],

Here, you check to see if the selected value passed is the same as that of the menu item, which can be true or false. You also pass the respective data list from the data.dart file to the mainNavAction() method call.

At this point, running the app should give you the output below with the menu item clicks working.

5. Building the App for Production

To get your Flutter web app into production, you first have to build and release a version of it. This will generate static files including JavaScript, HTML, and the various assets for the project. You can do that by running the command below from the root directory of your project using your terminal:

$ flutter build web

This will place the files into the /build/web directory of the project. You can serve these files like you would a static site with web servers such as Nginx, Apache, etc.

Conclusion

Following along with this tutorial, you’ve been able to successfully develop your first basic Flutter web application. The tutorial covered the origins of Flutter, its advantages, and reasons you should consider using Flutter for web application development.

It also covered working with widgets and creating your own widgets to build desired layouts and UIs. You also learned about some basic state management and responsiveness. Finally, you learned how to bundle and prepare your Flutter app for release online. This guide has given you a very solid foundation on what it takes to develop a Flutter web app and get it ready for production.

Rexford A. Nyarko.
Rexford A. Nyarko.

Written by

Written by

Rexford A. Nyarko

Rexford A. Nyarko

SHARE

SHARE

Build a Flutter Web App from Scratch: A Complete Guide

Title

Title

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.