Flutter Quick UI - Watch Shop

Flutter | Laravel | Angular
Hello guys, in this tutorial I am going to show you how to build a quick UI with flutter.
I am assuming you already know what flutter is. But if you don't, click on the link below to learn more about flutter.
To access the codes of the app we are about to build click the link below:
Let's Begin
Create A New Project
Assuming that you’ve installed Flutter and its required dependencies, we can proceed to the next step of creating a new Flutter project. Navigate to the directory where you want to store your project files and run this in the terminal:
$ flutter create watch_shop
In the code above, "flutter_watch_shop" represents the name of the project.
Launch The Project
To launch the project, in your terminal run the commands below:
$ cd flutter_watch_shop
$ flutter run
You should be greeted by a screen that looks like the image below.
UI Breakdown
Home Screen

Product Details Screen

Folder Structure
Here are the main project files that contain the application logic, I left out some files that were generated by the flutter create command that I didn't change.
* \flutter_watch_shop
* \assets
* \images
* \fonts
* \lib
* \global_widgets
* \models
* \services
* \utils
* \views
* app.dart
* main.dart
* router.dart
* theme.dart
Important Files
// pubspec.yaml
name: flutter_watch_shop
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
line_icons: ^0.2.0
flushbar: ^1.9.0
flutter_screenutil: ^0.5.3
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
assets:
- assets/images/logo.png
- assets/images/watch10.png
- assets/images/watch11.png
- assets/images/watch20.png
- assets/images/watch21.png
- assets/images/watch30.png
- assets/images/watch31.png
- assets/images/watch40.png
- assets/images/watch41.png
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/Poppins-Light.ttf
- asset: assets/fonts/Poppins-Regular.ttf
- asset: assets/fonts/Poppins-Medium.ttf
- asset: assets/fonts/Poppins-Bold.ttf
- asset: assets/fonts/Poppins-ExtraBold.ttf
For this project, I only used three packages
- Line Icons for external icons.
- Flushbar for alerts.
- Flutter Screen Util for responsivness.
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/app.dart';
void main() => runApp(App());
// theme.dart
import 'package:flutter/material.dart';
import 'utils/colors.dart';
import 'utils/utils.dart';
ThemeData buildThemeData() {
final baseTheme = ThemeData(fontFamily: AppFonts.primaryFont);
return baseTheme.copyWith(
primaryColor: AppColors.primaryColor,
scaffoldBackgroundColor: AppColors.scaffoldColor,
appBarTheme: AppBarTheme(
color: AppColors.appBarColor,
elevation: 0,
),
);
}
In the theme file, I defined the app-wide theme like the colors and fonts. To learn more about theming check out this link.
// router.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/views/home/home.dart';
import 'package:flutter_watch_shop/views/product_details/product_details.dart';
const String homeViewRoute = '/';
const String productDetailsViewRoute = 'product_details';
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homeViewRoute:
return MaterialPageRoute(builder: (_) => HomePage());
case productDetailsViewRoute:
return MaterialPageRoute(
builder: (_) => ProductDetailsPage(
product: settings.arguments,
),
);
break;
default:
return MaterialPageRoute(builder: (_) => HomePage());
}
}
In the router file, I defined all the available routes in the application. I used named routes. For more information check out this link
// app.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'router.dart' as router;
import 'theme.dart';
import 'utils/utils.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MaterialApp(
title: AppConstants.appName,
debugShowCheckedModeBanner: false,
theme: buildThemeData(),
onGenerateRoute: router.generateRoute,
initialRoute: router.homeViewRoute,
);
}
}
Global Widgets
My global widgets are widgets i created which could be used inside several other widgets.
// lib/global_widgets/custom_appbar.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/utils/utils.dart';
import 'package:line_icons/line_icons.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final bool isHome;
const CustomAppBar({Key key, this.isHome = true}) : super(key: key);
@override
Size get preferredSize => Size(null, 56.0);
@override
Widget build(BuildContext context) {
return AppBar(
leading: IconButton(
onPressed: () {
if(!isHome){
Navigator.pop(context);
}
},
icon: Icon(
isHome ? LineIcons.bars : LineIcons.angle_left,
color: Colors.black,
size: 28.0,
),
),
title: Image.asset(AppImages.logo, height: 20.0),
centerTitle: true,
actions: <Widget>[
IconButton(
onPressed: () {},
icon: Icon(
LineIcons.shopping_cart,
color: Colors.black,
size: 30.0,
),
)
],
);
}
}
// lib/global_widgets/product_card.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/models/product.dart';
import 'package:flutter_watch_shop/router.dart' as router;
import 'package:flutter_watch_shop/utils/colors.dart';
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({Key key, @required this.product}) : super(key: key);
@override
Widget build(BuildContext context) {
final spacer = SizedBox(height: 5.0);
final image = Hero(tag: product.id, child: Image.asset(product.photos[0]));
final name = Text(
product.name.toUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
);
final brand = Text(
product.brand.toUpperCase(),
style: TextStyle(fontSize: 11.0, color: Colors.grey),
);
final price = Text(
"\$${product.price.toString()}",
style: TextStyle(
fontWeight: FontWeight.bold,
),
);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 3.0),
child: MaterialButton(
color: AppColors.primaryLightColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
onPressed: () => Navigator.pushNamed(
context, router.productDetailsViewRoute,
arguments: product),
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
image,
spacer,
name,
spacer,
spacer,
brand,
spacer,
price
],
),
),
),
);
}
}
Models
There is only one model defined in the application which is the product model.
// lib/models/product.dart
import '../utils/utils.dart';
class Product {
int id;
String name;
List<String> photos;
List<String> colors;
int price;
String brand = "Daniel Wellington";
Product({
this.id,
this.name,
this.price,
this.photos,
this.colors,
});
}
List<Product> products = [
Product(
id: 1,
name: "Classic ST Mawes",
price: 179,
photos: [AppImages.watch10, AppImages.watch11],
colors: ["#E5AE87", "#C1C1C1"],
),
Product(
id: 2,
name: "Classic Bayswater",
price: 159,
photos: [AppImages.watch20, AppImages.watch21],
colors: ["#E5AE87", "#C1C1C1"],
),
Product(
id: 3,
name: "Classic Roselyn",
price: 159,
photos: [AppImages.watch30, AppImages.watch31],
colors: ["#E5AE87", "#C1C1C1"],
),
Product(
id: 4,
name: "Classic Cambridge",
price: 177,
photos: [AppImages.watch40, AppImages.watch41],
colors: ["#E5AE87", "#C1C1C1"],
),
];
Utils
The files in my utils directory are basically helper files and some files where constants are declared.
// lib/utils/colors.dart
import 'package:flutter/material.dart';
class AppColors {
static const primaryColor = const Color(0xFF353434);
static const primaryLightColor = const Color(0xFFEEEDED);
static const primaryDarkColor = const Color(0xFFE2E1E1);
static const statusBarColor = Colors.white;
static const appBarColor = primaryDarkColor;
static const scaffoldColor = primaryDarkColor;
}
// lib/utils/utils.dart
import 'dart:ui';
class AppConstants {
static const appName = "Watch Shop";
}
class AppFonts {
static const primaryFont = "Poppins";
}
class AppFunctions {
static Color formatColor(String color) {
var newColor = color.substring(1).toUpperCase();
var preffix = "0xFF";
var finalColor = int.parse(preffix + newColor);
return Color(finalColor);
}
}
class AppImages {
static const logo = "assets/images/logo.png";
static const watch10 = "assets/images/watch10.png";
static const watch11 = "assets/images/watch11.png";
static const watch20 = "assets/images/watch20.png";
static const watch21 = "assets/images/watch21.png";
static const watch30 = "assets/images/watch30.png";
static const watch31 = "assets/images/watch31.png";
static const watch40 = "assets/images/watch40.png";
static const watch41 = "assets/images/watch41.png";
}
Views
For my views, since a particular view could be split into several different widgets, I created directories for every view and inside those directories, I then created another directory for its widgets. An example is the home view which has a directory structure like:
* ..lib/views
* home
* \widgets
* product_list.dart
* home.dart
// lib/views/home/home.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/global_widgets/custom_appbar.dart';
import 'package:flutter_watch_shop/utils/utils.dart';
import 'package:flutter_watch_shop/views/home/widgets/product_list.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
TabController tabController;
List<String> tabs = ["Classic", "Sports", "Smart"];
@override
void initState() {
super.initState();
tabController = TabController(vsync: this, length: tabs.length);
}
@override
Widget build(BuildContext context) {
final tabBar = TabBar(
controller: tabController,
indicatorColor: Theme.of(context).primaryColor,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(width: 3.0),
insets: EdgeInsets.symmetric(horizontal: 55.0),
),
labelColor: Theme.of(context).primaryColor,
labelStyle: TextStyle(fontSize: 22.0, fontFamily: AppFonts.primaryFont),
unselectedLabelColor: Theme.of(context).primaryColor.withOpacity(0.3),
tabs: tabs.map((tabName) => Tab(text: tabName)).toList(),
);
final tabBarView = Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
margin: EdgeInsets.only(top: 30.0),
child: TabBarView(
controller: tabController,
children: <Widget>[ProductList(), ProductList(), ProductList()],
),
),
);
return Scaffold(
appBar: CustomAppBar(),
body: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(top: 50.0),
child: Column(
children: <Widget>[tabBar, tabBarView],
),
),
);
}
}
// lib/views/home/widgets/product_list
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/global_widgets/product_card.dart';
import 'package:flutter_watch_shop/models/product.dart';
class ProductList extends StatefulWidget {
@override
_ProductListState createState() => _ProductListState();
}
class _ProductListState extends State<ProductList> {
List<String> filters = [
'Trending',
'Popular',
'Lowest Price',
'Highest Price'
];
String selectedFilter;
@override
void initState() {
super.initState();
selectedFilter = "Trending";
}
@override
Widget build(BuildContext context) {
final itemCountRow = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"270 Items",
style: TextStyle(fontSize: 17.0),
),
DropdownButton(
value: selectedFilter,
items: filters.map((value) {
return DropdownMenuItem(
value: value,
child: Text(value, style: TextStyle(fontSize: 17.0)),
);
}).toList(),
onChanged: (selected) {
setState(() {
selectedFilter = selected;
});
},
)
],
);
final list = Expanded(
child: GridView.builder(
itemBuilder: (BuildContext context, int index) {
return ProductCard(product: products[index]);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 15.0,
mainAxisSpacing: 15.0,
childAspectRatio: 0.65,
),
itemCount: products.length,
),
);
return Container(
child: Column(
children: <Widget>[itemCountRow, list],
),
);
}
}
// lib/views/product_details/product_details.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_watch_shop/global_widgets/custom_appbar.dart';
import 'package:flutter_watch_shop/models/product.dart';
import 'package:flutter_watch_shop/services/alert.service.dart';
import 'package:flutter_watch_shop/utils/colors.dart';
import 'package:flutter_watch_shop/views/product_details/widgets/color_chooser.dart';
class ProductDetailsPage extends StatefulWidget {
final Product product;
const ProductDetailsPage({Key key, @required this.product}) : super(key: key);
@override
_ProductDetailsPageState createState() => _ProductDetailsPageState();
}
class _ProductDetailsPageState extends State<ProductDetailsPage> {
int _selectedColorIndex = 0;
@override
Widget build(BuildContext context) {
final double screenHeight = MediaQuery.of(context).size.height;
final double screenWidth = MediaQuery.of(context).size.width;
ScreenUtil.instance = ScreenUtil(
width: 388,
height: 1600,
allowFontScaling: true,
)..init(context);
final multiplier = screenHeight / screenWidth;
final spacer = SizedBox(height: 10.0);
final image = Hero(
tag: widget.product.id,
child: Image.asset(
widget.product.photos[_selectedColorIndex],
height: ScreenUtil().setHeight(400) * multiplier,
),
);
final name = Text(
widget.product.name.toUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w500,
),
);
final brand = Text(
widget.product.brand.toUpperCase(),
style: TextStyle(fontSize: 14.0, color: Colors.grey),
);
final chooseColor = Text(
"Choose a Color",
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w500),
);
final colorChooser = ColorChooser(
colors: widget.product.colors,
selectedColorIndex: _selectedColorIndex,
onColorSelected: (int selected) {
setState(() {
_selectedColorIndex = selected;
});
},
);
final top = Expanded(
child: Container(
padding: EdgeInsets.only(top: 50.0),
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.scaffoldColor,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(30.0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
name,
spacer,
brand,
image,
chooseColor,
colorChooser
],
),
),
);
final bottom = Container(
height: MediaQuery.of(context).size.height * 0.12, // 95.0
color: Colors.white,
child: MaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20.0),
)),
onPressed: () {
AlertService().showAlert(
context: context,
message: "${widget.product.name} has been added to cart.",
type: AlertType.success,
);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Add To Cart - ",
style: TextStyle(
fontSize: 22.0,
),
),
Text(
"\$${widget.product.price.toString()}",
style: TextStyle(fontSize: 22.0, color: Colors.grey[600]),
),
],
),
),
);
return Scaffold(
backgroundColor: Colors.white,
appBar: CustomAppBar(isHome: false),
body: Column(
children: <Widget>[top, bottom],
),
);
}
}
Home page preview

// lib/views/product_details/widgets/color_chooser.dart
import 'package:flutter/material.dart';
import 'package:flutter_watch_shop/utils/utils.dart';
class ColorChooser extends StatelessWidget {
final List<String> colors;
final int selectedColorIndex;
final Function(int) onColorSelected;
const ColorChooser({
Key key,
@required this.colors,
@required this.onColorSelected,
@required this.selectedColorIndex,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 10.0),
height: 50.0,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: colors.map((color) {
int currentIndex = colors.indexOf(color);
bool isSelected = currentIndex == selectedColorIndex;
return colorBall(
color: color,
isSelected: isSelected,
onSelected: () => onColorSelected(currentIndex),
);
}).toList(),
),
);
}
Widget colorBall({String color, bool isSelected, Function onSelected}) {
final checked = isSelected
? Icon(
Icons.check,
color: Colors.white,
)
: Container();
return MaterialButton(
elevation: isSelected ? 4.0 : 0.0,
minWidth: 60.0,
color: AppFunctions.formatColor(color),
splashColor: AppFunctions.formatColor(color),
shape: CircleBorder(),
onPressed: onSelected,
child: checked,
);
}
}
Product details page preview

Thanks for reading 😊
