Skip to main content

Command Palette

Search for a command to run...

Flutter Quick UI - Watch Shop

Updated
0 min read
Flutter Quick UI - Watch Shop
E

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 Home Page.png

Product Details Screen Details Page.png

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

// 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

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

Product details page preview

Thanks for reading 😊