Learn More

Try out the world’s first micro-repo!

Learn More

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 web 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 Flutter Starter 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<dynamic,dynamic>` 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<String,dynamic>`. Therefore, for a list of JSON objects that is a list of maps, you type `List<Map<String,dynamic>>`.

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

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.

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

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.

Table of Contents

Flutter

Dart

Front End

More from Pieces
Subscribe to our newsletter
Join our growing developer community by signing up for our monthly newsletter, The Pieces Post.

We help keep you in flow with product updates, new blog content, power tips and more!
Thank you for joining our community! Stay tuned for the next edition.
Oops! Something went wrong while submitting the form.