A complete Shopping App using Flutter-Part 1

Sanjib Maharjan
13 min readJun 27, 2023

--

In this tutorial, we’ll be attempting to develop a shopping app by using Flutter for Ios, Android, and web platforms. We’ll be doing this in several parts and this is the first part where we will initialize and prepare some portion of the UI. The final form of the app is as shown below, the full-fledged App using the Mock API call.

We will be using fvm and If you prefer the plane flutter command, remove the ‘fvm’ from all the commands below. Please go through this article for setting up fvm and to learn about its use cases. Let's create our project by using the command below:

fvm flutter create flutter_shopping_app

If you are not using fvm then the above command would be:

flutter create flutter_shopping_app

Let's define some theme colors that we’ll be using in the app inside the package ‘constant/color.dart’:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

const ThemeColor = Color(0xFF52B36A);
// const ThemeColor = Color(0xFF6CAE7A);
const ThemeColorLight = Color(0xFF91E7AE);
const ThemeColorLighter = Color(0xFFB0E7AE);

const ThemeTextColor = Color(0xFF5A5C62);
const ThemeTextColorLight = Color(0xFF9B9FA8);
const ThemeTextColorLighter = Color(0xFFC0C5D0);
const ThemeTextColorLightest = Color(0xFFF5F5F5);

const MaterialColor ThemeWhite = const MaterialColor(
0xFFFFFFFF,
const <int, Color>{
50: const Color(0xFFFFFFFF),
100: const Color(0xFFFFFFFF),
200: const Color(0xFFFFFFFF),
300: const Color(0xFFFFFFFF),
400: const Color(0xFFFFFFFF),
500: const Color(0xFFFFFFFF),
600: const Color(0xFFFFFFFF),
700: const Color(0xFFFFFFFF),
800: const Color(0xFFFFFFFF),
900: const Color(0xFFFFFFFF),
},
);

Remove all the boilerplate code from the ‘main.dart file and replace them with the following:

import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/constant/color.dart';
import 'package:flutter_shopping_app/ui/dashboard/dashboard.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shopping App Demo',
theme: ThemeData(
primarySwatch: ThemeWhite,
),
home: Scaffold(
body: Dashboard(),
appBar: AppBar(
centerTitle: false,
title: Text("Shopping App"),
elevation: 0,
),
drawer: Drawer(),
),
);
}
}

Here the Dashboard class is located in the ‘ui/dashboard/dashboard.dart’ file. The contents of the files are:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/constant/color.dart';

class Dashboard extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
cursorColor: ThemeColor,
decoration: InputDecoration(
isDense: true,
errorBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
focusedErrorBorder: OutlineInputBorder(
// borderSide: BorderSide(color: Colors.red),
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
fillColor: ThemeTextColorLightest,
filled: true,
hintText: "Search ...",
border: InputBorder.none,
prefixIcon: Icon(
Icons.search,
color: Colors.grey,
)),
),
Text("Dashboard"),
],
);
}
}

This is the result of the code above. We are far from the final form of the project which we’ll achieve only if we continue this without feeling lazy. We have a lot of code to work on, so without further ado, let's continue by first creating a custom class for TextFormField. The code above for the Search field is very long so we’ll be making a custom class to reuse it later without repeating the code.

The code for ‘ui/common/custom_text_field.dart’ is:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_shopping_app/constant/color.dart';

class CustomTextField extends StatelessWidget {
const CustomTextField(
{Key key,
this.onTap,
this.label,
this.onSubmitted,
this.onChanged,
this.controller,
this.enabled: true,
this.placeholder,
this.keyboardType,
this.suffixIcon,
this.obscureText: false,
this.readOnly: false,
this.filled: true,
this.prefixIcon,
this.errorMaxLines: 1,
this.maxLines: 1,
this.focusedColor: ThemeColor,
this.errorText: '',
this.padding,
this.valid: true,
this.inputFormatters})
: super(key: key);

final TextEditingController controller;
final String placeholder;
final String errorText;
final String label;
final bool filled;
final int errorMaxLines;
final int maxLines;
final Widget prefixIcon;
final Widget suffixIcon;
final void Function(String) onSubmitted;
final void Function(String) onChanged;
final bool enabled;
final bool valid;
final bool readOnly;
final bool obscureText;
final TextInputType keyboardType;
final VoidCallback onTap;
final Color focusedColor;
final EdgeInsets padding;
final List<TextInputFormatter> inputFormatters;

@override
Widget build(BuildContext context) {
return TextField(
cursorColor: ThemeColor,
controller: controller,
textInputAction: TextInputAction.go,
onSubmitted: onSubmitted,
inputFormatters: inputFormatters,
maxLines: maxLines,
onChanged: onChanged,
decoration: InputDecoration(
errorText: valid ? null : errorText,
errorMaxLines: errorMaxLines,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
filled: filled,
fillColor: ThemeTextColorLightest,
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: focusedColor),
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(
const Radius.circular(10.0),
)),
// counter: SizedBox.shrink(),
hintText: placeholder,
labelText: this.label,
isDense: true,
contentPadding: padding,
),
obscureText: obscureText,
keyboardType: keyboardType,
enabled: enabled,
readOnly: readOnly,
autocorrect: false,
onTap: onTap
);
}
}

The code in the “dashboard.dart” file is reduced to:

...,
CustomTextField(
prefixIcon: Icon(
Icons.search,
color: Colors.grey,
),
placeholder: 'Search ...',
),
...

Next, we’ll proceed to create the shop category list module which is a horizontal list. First, create a file ’ui/category/category_list.dart’ with the following content:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CategoryList extends StatelessWidget {

@override
Widget build(BuildContext context) {
return SizedBox(
height: 60,
child: ListView(
scrollDirection: Axis.horizontal,
children: [Text("Fruits"), Text("Vegetables"), Text("Dairy")]),
);
}
}

Consider the following JSON consisting of three properties: title, image, and theme color.

{
"title": "Fruits",
"imageUrl":
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
"theme": 0xFFF9E3FD
}

Let's prepare some mock data to represent dummy category list data as:

abstract class MockUtil {
static List<Map> getMockAppCategories() {
return [
{
"title": "Fruits",
"imageUrl":
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
"theme": 0xFFF9E3FD
},
{
"title": "Vegetables",
"imageUrl":
"https://www.pngjoy.com/pngl/123/2510559_funny-emoji-apple-emoji-broccoli-png-download.png",
"theme": 0xFFE9FBE5
},
{
"title": "Dairy",
"imageUrl":
"https://cdn.iconscout.com/icon/free/png-256/cheese-1806482-1534540.png",
"theme": 0xFFFEFFE5
},
{
"title": "Meat",
"imageUrl":
"https://img.icons8.com/emoji/452/cut-of-meat-emoji.png",
"theme": 0xFFFCEFEB
},
{
"title": "Vegetables",
"imageUrl":
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
"theme": 0xFFF9E3FD
},
{
"title": "Dairy",
"imageUrl":
"https://cdn.iconscout.com/icon/free/png-256/cheese-1806482-1534540.png",
"theme": 0xFFFEFFE5
},
{
"title": "Meat",
"imageUrl":
"https://img.icons8.com/emoji/452/cut-of-meat-emoji.png",
"theme": 0xFFFCEFEB
}
];
}
}

Similarly, create a ‘ui/category/category_item_card.dart’ file as:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CategoryItemCard extends StatelessWidget {
final String title;
final String imageUrl;
final int themeColor;

CategoryItemCard({Key key, this.title, this.imageUrl, this.themeColor})
: super(key: key);

@override
Widget build(BuildContext context) {
return Container(
height: 60,
width: 80,
padding: EdgeInsets.all(6),
margin: EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Color(themeColor)),
child: Column(
children: [
Expanded(child: Image.network(imageUrl)),
SizedBox(
height: 6,
),
Text(
title ?? "Title",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, ),
),
],
),
);
}
}

We can then update the horizontal list as:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/category/category_item_card.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';

class CategoryList extends StatelessWidget {
final String title;


CategoryList({Key key, this.title})
: super(key: key);

@override
Widget build(BuildContext context) {
final List _category = MockUtil.getMockAppCategories();
return SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _category.length,
itemBuilder: (context, index) => CategoryItemCard(
title: _category[index]["title"],
imageUrl: _category[index]["imageUrl"],
themeColor: _category[index]["theme"],
),
),
);
}
}

Later we’ll be replacing the final List _category = MockUtil.getMockAppCategories(); with the mock API call.

Now let's use a modal class to represent the data title, imageUrl, and theme color in ‘ui/category/data/model/category_item.dart’:

class CategoryItem {
final String title;
final String imageUrl;
final int theme;

CategoryItem({this.title, this.imageUrl, this.theme});
}

We’ll be using the library ‘json_annotation’ for JSON serialization. The class is updated to follow with the auto-generation of the ‘category_item.g.dart’ file. Check out this article to know about serialization.

import 'package:json_annotation/json_annotation.dart';

part 'category_item.g.dart';

@JsonSerializable()
class CategoryItem {
final String title;
final String imageUrl;
final int theme;

CategoryItem({this.title, this.imageUrl, this.theme});

factory CategoryItem.fromJson(Map<String, dynamic> json) =>
_$CategoryItemFromJson(json);

Map<String, dynamic> toJson() => _$CategoryItemToJson(this);

String toJsonString() {
return '''{
"title": "$title",
"imageUrl": "$imageUrl",
"theme": "$theme"
}''';
}
}

Then we can replace the MockUtil class as:

import 'package:flutter_shopping_app/ui/category/data/model/category_item.dart';

abstract class MockUtil {
static List<CategoryItem> getMockAppCategories() {
return [
CategoryItem(
title: "Fruits",
imageUrl:
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
theme: 0xFFF9E3FD),
CategoryItem(
title: "Vegetables",
imageUrl:
"https://cdn.iconscout.com/icon/free/png-256/cheese-1806482-1534540.png",
theme: 0xFFE9FBE5),
CategoryItem(
title: "Dairy",
imageUrl:
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
theme: 0xFFFEFFE5),
CategoryItem(
title: "Meat",
imageUrl: "https://img.icons8.com/emoji/452/cut-of-meat-emoji.png",
theme: 0xFFFCEFEB),
CategoryItem(
title: "Fruits",
imageUrl: "https://img.icons8.com/emoji/452/cut-of-meat-emoji.png",
theme: 0xFFF9E3FD),
CategoryItem(
title: "Fruits",
imageUrl:
"https://i.pinimg.com/originals/fa/07/04/fa0704ba240c7b884b9b7ee94157fb7d.png",
theme: 0xFFF9E3FD),
];
}
}

Then we have to make some changes in the “category_list.dart” file as the result of the changes in the MockUtil class.

This is the result we have achieved so far. The gap between the components is not looking good which we’ll be working on after we’ve completed designing all of the modules.

Next, we’ll be working on displaying some banners to highlight different offers that are currently available in the app. We’ll be using some images that I‘ve randomly picked from the web and by using library carousel_slider we’ll display them in the app.

Add the following content in the MockUtil class to get the list of offers that is currently available.

static List<String> getOfferBanners() {
return [
"https://www.kenresearch.com/uploads/posts/images/Herbal%20Products%20Market.jpg",
"https://images.unsplash.com/photo-1506617564039-2f3b650b7010?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8Z3JvY2VyeXxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80",
"https://apnafamilystore.com/shop-online-banner.jpg",
];
}

Then create a “ui/common/carousel/custom_carousel.dart” file with the following content.

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';

class CustomCarousel extends StatelessWidget {
final List<String> _banners = MockUtil.getOfferBanners();

@override
Widget build(BuildContext context) {
return CarouselSlider.builder(
itemCount: _banners.length,
options: CarouselOptions(
enlargeStrategy: CenterPageEnlargeStrategy.scale,
aspectRatio: 3,
viewportFraction: 1,
),
itemBuilder: (BuildContext context, int itemIndex, int realIndex) =>
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
_banners[itemIndex],
fit: BoxFit.cover,
width: double.infinity,
),
),
);
}
}

Let's work on the module which will be used to display the list of products. Let's start by adding a file “ui/common/item/item_card.dart”

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/constant/color.dart';

class ItemCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 120,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // changes position of shadow
),
], borderRadius: BorderRadius.circular(10), color: Colors.white),
child: Column(
children: [
Image.network(
'https://creazilla-store.fra1.digitaloceanspaces.com/cliparts/1502591/cabbage-clipart-md.png',
height: 100,
),
Align(
child: Text(
"Cabbages",
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
alignment: Alignment.centerLeft,
),
SizedBox(height: 4,),
Row(
children: [
Text("\$5.5",
style: TextStyle(fontSize: 12, color: ThemeTextColor)),
SizedBox(
width: 4,
),
Expanded(
child: Text(
"per piece",
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10, color: ThemeTextColorLight),
),
),
],
)
],
),
);
}
}

This is the UI for showcasing the item in the dashboard, which results from the above code. We need the image URL, currency, currency type, name, and selling unit for this. We’ll also be adding discounts for later use. For holding these properties let's create a modal class “ui/common/item/data/model/product_item.dart” using JSON serialization.

import 'package:json_annotation/json_annotation.dart';

part 'product_item.g.dart';

@JsonSerializable()
class ProductItem {
final String name;
final String imageUrl;
final double currency;
final double discount;

final String currencyType;
final String sellingUnit;

String get price =>"$currencyType$currency";

ProductItem({this.name, this.imageUrl, this.currency, this.currencyType: "\$", this.discount, this.sellingUnit});

factory ProductItem.fromJson(Map<String, dynamic> json) =>
_$ProductItemFromJson(json);

Map<String, dynamic> toJson() => _$ProductItemToJson(this);

String toString() {
return '''{
"name": "$name",
"imageUrl": "$imageUrl"
}''';
}
}

Also, create the following methods in the MockUtil class, giving the list of items we are showcasing.

static List<ProductItem> getTrendingItems() {
return [
ProductItem(
name: "Cabbages",
imageUrl:
"https://creazilla-store.fra1.digitaloceanspaces.com/cliparts/1502591/cabbage-clipart-md.png",
currency: 9.9,
currencyType: "\$",
sellingUnit: "per piece",
discount: 10),
ProductItem(
name: "Tomatoes",
imageUrl: "https://pngimg.com/uploads/tomato/tomato_PNG12594.png",
currency: 7.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10),
ProductItem(
name: "Potatoes",
imageUrl:
"https://www.freeiconspng.com/uploads/slice-of-potato-png-5.png",
currency: 9.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10),
ProductItem(
name: "Strawberries",
imageUrl:
"https://www.freeiconspng.com/uploads/strawberry-png-9.png",
currency: 9.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10)
];
}

static List<ProductItem> getFeaturedItems() {
return [
ProductItem(
name: "Carrots",
imageUrl:
"https://www.transparentpng.com/thumb/carrot/71HwEm-fresh-carrot-photos.png",
currency: 19.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10),
ProductItem(
name: "Banana",
imageUrl: "https://lh3.googleusercontent.com/proxy/BOW44p3gAoQxVN-8ZyHW2GPNNMSHdTpt_pMhCRdv626T9PwZrOehRmGRkx0yO1tNwVYXMPe6fiCUaSSu9AulQhmOMPhxxoQk8cJdV02w7aS4Z6eIhD2_WpJvQagliJJapQOiANFD8agNAc0ujavJXjDSvWIPQ1Pl6rD9Lg",
currency: 7.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10),
ProductItem(
name: "Beef",
imageUrl:
"https://lh3.googleusercontent.com/proxy/VOAWvymbQFdyjhV-HwIr2p6frGZ3837LC4n9h6BiohPHThxmYaWFVzqTj_06J3Ya7bAaC83q0_DeObwEOOsmSp0wnp8T33LKZ7Uofw7R8qUu-5hKSHt9wlYlsRHZwtOl8HA",
currency: 49.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 10),
ProductItem(
name: "Pineapple",
imageUrl:
"https://www.freepnglogos.com/uploads/pineapple-png/pineapple-transparent-png-clip-art-image-gallery-24.png",
currency: 7,
currencyType: "\$",
sellingUnit: "per piece",
discount: 5)
];
}

static List<ProductItem> getTopSellingItems() {
return [
ProductItem(
name: "Peas",
imageUrl:
"http://pngimg.com/uploads/pea/small/pea_PNG24285.png",
currency: 17.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 7),
ProductItem(
name: "Chips",
imageUrl: "https://lh3.googleusercontent.com/proxy/xX0Ik3kBDXlfWECuyyK3wCq_j5t26741tyrJ_Uq1m-eFfjryTOPKBMdy6pVgmBvmhqrz7rhhjsc9aNvNZOJqSfns_J-X2NAoi0p9HV9Wwm6x22xexTCcoio-cwY8KarQ",
currency: 7.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 12),
ProductItem(
name: "Corn",
imageUrl:
"https://freepngimg.com/thumb/categories/951.png",
currency: 49.9,
currencyType: "\$",
sellingUnit: "per kg",
discount: 15),
ProductItem(
name: "Spinach",
imageUrl:
"https://lh3.googleusercontent.com/proxy/5_YRMt-pzx7mbns4dNmuTipS0AanpL1nt6mL5E4fPQo70gOsDcxm7GHZQ-pP0AiXjC0wAmvvRn0ORuLkSmxjZZcIDZrsZ_voFfRAZ9ix",
currency: 7,
currencyType: "\$",
sellingUnit: "per piece",
discount: 5)
];
}

Also, update the “ui/common/item/item_card.dart” file as follows.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/constant/color.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';

class ItemCard extends StatelessWidget {
final ProductItem item;

const ItemCard({Key key, this.item}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
width: 120,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1), // changes position of shadow
),
], borderRadius: BorderRadius.circular(10), color: Colors.white),
child: Column(
children: [
Image.network(
item.imageUrl,
height: 100,
),
Align(
child: Text(
item.name,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
alignment: Alignment.centerLeft,
),
SizedBox(
height: 4,
),
Row(
children: [
Text(item.price,
style: TextStyle(fontSize: 12, color: ThemeTextColor)),
SizedBox(
width: 4,
),
Expanded(
child: Text(
item.sellingUnit,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10, color: ThemeTextColorLight),
),
),
],
)
],
),
);
}
}

Now create a “ui/common/item/item_list.dart” file to display the trending, top-selling, and featured items from MockUtil class.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';

class ItemList extends StatelessWidget {
final List<ProductItem> items;

const ItemList(this.items, {Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
return SizedBox(
height: 156,
child: ListView.builder(
itemBuilder: (context, index) => ItemCard(
item: items[index],
),
itemCount: items.length,
scrollDirection: Axis.horizontal,
),
);
}
}

Make the following changes in the “dashboard.dart” file:

class Dashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
CustomTextField(
prefixIcon: Icon(
Icons.search,
color: Colors.grey,
),
placeholder: 'Search ...',
),
CategoryList(),
CustomCarousel(),
ItemList(MockUtil.getTrendingItems()),
ItemList(MockUtil.getFeaturedItems()),
ItemList(MockUtil.getTopSellingItems()),
],
);
}
}

This is the result displaying 3 different sections trending, top-selling, and featured items but from UI we couldn't distinguish between them, rather it looks like the same list displayed in a grid. For this, we’ll be adding a title section for each list.

This is the result displaying 3 different sections trending, top-selling, and featured items but from UI we couldn’t distinguish between them, rather it looks like the same list displayed in a grid. For this, we’ll be adding a title section for each list.

Make changes in item_list.dart file as:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';

class ItemList extends StatelessWidget {
final List<ProductItem> items;
final String title;

const ItemList(this.items, {Key key, this.title: ""}) : super(key: key);

_seeAll() {}

@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
InkWell(
child: Text("see all"),
onTap: _seeAll,
),
],
),
),
SizedBox(
height: 158,
child: ListView.builder(
itemBuilder: (context, index) => ItemCard(
item: items[index],
),
itemCount: items.length,
scrollDirection: Axis.horizontal,
),
),
],
);
}
}

Similarly, make the following changes to the “dashboard.dart” file.

...
ItemList(MockUtil.getTrendingItems(), title: "Trending",),
ItemList(MockUtil.getFeaturedItems(), title: "Featured",),
ItemList(MockUtil.getTopSellingItems(), title: "Top Selling",),

Now the UI looks a little bit better, but the gap between different components is not up to the mark. For this, we’ll be adding top margin in each of them to make sure that there is some gap between each of the components in the view while will definitely contribute to the look and feel of the entire application.

Now the UI looks a little bit better, but the gap between different components is not up to the mark. For this, we’ll be adding a top margin in each of them to make sure that there is some gap between each of the components in the view which will definitely contribute to the look and feel of the entire application.

Now make changes in the “custom_carousel.dart” file as:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';

class CustomCarousel extends StatelessWidget {
final List<String> _banners = MockUtil.getOfferBanners();

final double topMargin;

CustomCarousel({Key key, this.topMargin: 10}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: topMargin),
child: CarouselSlider.builder(
itemCount: _banners.length,
options: CarouselOptions(
enlargeStrategy: CenterPageEnlargeStrategy.scale,
aspectRatio: 3,
viewportFraction: 1,
),
itemBuilder: (BuildContext context, int itemIndex, int realIndex) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
_banners[itemIndex],
fit: BoxFit.cover,
width: double.infinity,
),
),
),
),
);
}
}

Similarly, make the following changes in the “category_list.dart” file:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/category/category_item_card.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';
import 'package:flutter_shopping_app/ui/category/data/model/category_item.dart';

class CategoryList extends StatelessWidget {
final String title;
final double topMargin;

CategoryList({Key key, this.title, this.topMargin: 10})
: super(key: key);

@override
Widget build(BuildContext context) {
final List<CategoryItem> _category = MockUtil.getMockAppCategories();
return Padding(
padding: EdgeInsets.only(top: topMargin),
child: SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _category.length,
itemBuilder: (context, index) => CategoryItemCard(
title: _category[index].title,
imageUrl: _category[index].imageUrl,
themeColor: _category[index].theme,
),
),
),
);
}
}

And also in the “item_list.dart” file, make the following changes:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';

class ItemList extends StatelessWidget {
final List<ProductItem> items;
final String title;
final double topMargin;

const ItemList(this.items, {Key key, this.title: "", this.topMargin: 10})
: super(key: key);

_seeAll() {}

@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(10, topMargin, 10, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
InkWell(
child: Text("see all"),
onTap: _seeAll,
),
],
),
),
SizedBox(
height: 158,
child: ListView.builder(
itemBuilder: (context, index) => ItemCard(
item: items[index],
),
itemCount: items.length,
scrollDirection: Axis.horizontal,
),
),
],
);
}
}

Finally, in the “dashboard.dart” file make the changes as:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/category/category_list.dart';
import 'package:flutter_shopping_app/ui/common/carousel/custom_carousel.dart';
import 'package:flutter_shopping_app/ui/common/form/custom_text_field.dart';
import 'package:flutter_shopping_app/ui/common/item/item_list.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';

class Dashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
child: CustomTextField(
prefixIcon: Icon(
Icons.search,
color: Colors.grey,
),
placeholder: 'Search ...',
),
),
CategoryList(),
CustomCarousel(),
ItemList(MockUtil.getTrendingItems(), title: "Trending",),
ItemList(MockUtil.getFeaturedItems(), title: "Featured",),
ItemList(MockUtil.getTopSellingItems(), title: "Top Selling",),
],
);
}
}

This is the final form of UI for this part of the tutorial. Of course, we can add other components as per the requirement and designs of our app.

In the 2nd part of the tutorial, we’ll try and replace the hardcoded data with state management functions of the flutter_bloc library. Do check out the 2nd part and feel free to leave comments for any suggestions and tips to improve this project.

Project Completed Link: https://github.com/cshanjib/flutter_shopping_app/tree/section1-end

Next - Part 2: API calls and Dependency Injection using the get_it package

Check out other tutorials in this series:

Part 1: Designing the E-commerce App
Part 2: API calls and Dependency Injection using the get_it package
Part 3: Authentication and Login Design using the flutter_bloc package
Part 4: Responsive Design
Part 5: Proper Routing in Flutter Web using beamer package
Part 6: Pagination in Flutter(Lazy Loading, Paginated Truncated Display, etc.)

Next, we’ll be working on API calls, dependency injection, etc and you can it in this link. In the meantime, if you like this tutorial please hit 👏 as many times as you like and also leave comments if you have any questions or suggestions. Thank you!

#flutter, #flutter_app_development, #ecommerce_application, #flutter_ui_tutorial

--

--