Building a Flutter Web App from Scratch: A Complete Guide
Written By: Rexford A. Nyarko
Flutter is a development framework that allows developers to build applications across various platforms, namely Android, iOS, and now 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. 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 to the application.
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 very 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?
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 also learn how to bundle and export your application 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:
- 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 your installation and dependencies are properly set up by running the following:
$ flutter doctor
6. Enable Flutter 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 (2.10.4 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 development environment all set up, you’re ready to start building your Flutter app. 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 guide as it will help you learn most of the fundamentals of web development in Flutter.
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 UI that’s as close as possible to the sample artwork.
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 web application 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 upon which you can build. 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:
You can now change directory to the project directory and run the basic app in your Chrome browser. After which, you can switch to the IDE of your choice (Android Studio was used 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:
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 app you’ll be presented with a blank page. Time to build your UI.
2. Data and Assets
Before you start building the app, you need to add some fonts and static images to your application, such as the background image file to be used as the background. 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 A1fonts:
- 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<Map> genres = [];
const List<Map<String,dynamic>> newReleases = [];
const List<Map<String,dynamic>> mostPopular = [];
const List<Map<String,dynamic>> recommended = [];
const List<Map<String,dynamic>> 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
, andtopChart
are all lists of movies for the menu items “New Releases,” “Most Popular,” “Recommended,” and “Top Chart,” respectively.
Note: In the Dart language, 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 your layout and UI.
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.
3.1 Creating the Overall App Layout
You need to create a main widget that will provide the overall structure in which the various other 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, which is mainly to introduce some level of control or specific visual behaviors and effects, but not affect the main structure.
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<StatefulWidget> createState() {
return _AppLayoutState() ;
}
}
/// b. Creating state for stateful widget
class _AppLayoutState extends State<AppLayout>{
@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 a 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.
3.2 The Left Pane
Create a directory inside the lib
directory of your project root and name it widgets
. Create another directory in that 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 of white for color and 20 for font size, 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 a 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 for the sub-navigation menu items 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 thetitle
without the icons and it will be displayed as one of the smaller menu items. If you do however provide icons, they’ll appear in their respective positions. - Also, the default text size is 18, which is smaller. Therefore, for a bigger menu item size like the first one, you can specify the
size
parameter when creating the widget.
Aside from these, the rest are just as explained for the MainNavItem
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 aContainer
defined with a height of 170 and a decoration argument defining anAssetImage
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 theCrossAxisAlignment
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 theColumn
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 fourSubNavItem
widgets. And just as you defined, this widget takes aString
for the title of the menu item, adouble
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 of 18 defined in the widget.
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.
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 aText
, aCircleAvatar
, and anIcon
widget separated bySizedBox
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 anAssetImage
widget with the path to your profile thumbnail photo file. This could also be aNetworkImage
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 of 35. - The next is the
Icon
widget, where you specify a settings icon with white color. Ideally, it should be able to respond to clicks and, therefore, should have been wrapped with aMaterialButton
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:
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 aprefixIcon
, which is essentially an icon that’s shown before the input box. You specify theIcon
widget with the search icon and wrap them inside aPadding
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 thisTextField
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 in 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.
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 what you did for 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<String>(
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
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 theText
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 theitems
parameter of that widget as seen above. Here you add only oneDropdownMenuItem
, 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, and these are separated with some spacing using the SizedBox
. For the purposes of this 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<String>(
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.
3.5 The Main Pane
This widget is the part of this 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<Map<String,dynamic>> 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 aGridView
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 is used 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 aSliverGridDelegate
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 assigned to thecrossAxisSpacing
andmainAxisSpacing
. - 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 aBuildContext
and anindex
(of the current item in the data list) and returns a widget. Here, you return aColumn
widget containing aFlexible
widget and aContainer
widget. - Creating rounded corners with ClipRRect: The
Flexible
widget contains aClipRRect
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 theborderRadius
parameter of this widget. - The
GridTile
widget is assigned anImage
widget as a child and aContainer
widget as a footer. As the tile images are going to be dynamically loaded over the network, aNetworkImage
is used as a provider to theImage
widget. The URL string to the image file is specified by concatenating the base URL from the value ofpImageBase
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 outerContainer
widget is used to align the contents to the center left using thealignment
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<int> 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:
/// Main Pane
Expanded(
child: Center(
child: MainPane(data: topChart) //here
),
)
Running the app should now give you an output similar to the following image.
4. Adding Basic Functionality
As stated earlier in this tutorial, the goal is to achieve basic functionality in the application. 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 with 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<AppLayout>{
List<Map<String,dynamic>> 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<Map<String,dynamic>> 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 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 why you should consider using Flutter for web application development.
It also covered working with widgets as well as creating your own widgets to build desired layouts and UIs. You also learned about some basic state management and responsiveness. Finally, you learned about how to bundle and prepare your app for release online. This guide will have given you a very solid foundation on what it takes to develop a Flutter web application and get it ready for production.