A complete Shopping App using Flutter-Part 2

Sanjib Maharjan
17 min readJun 27, 2023

This is the second part of our shopping app tutorial in which we’ll be using the flutter_bloc, get_it, injector, etc packages to fetch the data from API calls. Of course, the API calls are mocked but we’ll try to design the functions in such a way that only minimal changes are required to replace them with the real API calls.

Please consider checking the first part if you haven't yet and the tutorial on dependency injection if you need to go through this tutorial without any hassle.

First of all, let's add the dependencies that we’ll be required in this part. Make sure to add the versions as mentioned below or be extra careful as some versions might not be compatible with one another.

dependencies:
...
json_annotation: ^3.1.1
carousel_slider: ^3.0.0
injectable: ^1.0.7
get_it: ^5.0.6
equatable: ^1.2.6
flutter_bloc: ^6.1.2
dev_dependencies:
...
injectable_generator: ^1.0.6
build_runner: ^1.10.6
json_serializable: ^3.5.0

Add an “injectable/config.dart” file for configuring the get_it packages with the following contents:

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final getIt = GetIt.instance;
@injectableInit
GetIt configureDependencies() => $initGetIt(getIt);

Now run “flutter packages pub run build_runner build” to generate the necessary missing files. Then import the generated import in “injectable/config.dart” and add the following content in the “main.dart” file.

...
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureDependencies();
runApp(MyApp());
}
...

Next, we’ll be working on the mock data providers for fetching different types of product lists. Create the “ui/common/item/data/provider/product_item_provider.dart” file with the following contents.

import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';
import 'package:injectable/injectable.dart';

abstract class ProductItemProvider{
Future<List<ProductItem>> getTrendingProducts();
Future<List<ProductItem>> getTopSellingProducts();
Future<List<ProductItem>> getFeaturedProducts();
}

@Named("mock")
@Singleton(as: ProductItemProvider)
class MockProductItemProvider implements ProductItemProvider {
const MockProductItemProvider();

@override
Future<List<ProductItem>> getTrendingProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getTrendingItems());
}

@override
Future<List<ProductItem>> getFeaturedProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getFeaturedItems());
}

@override
Future<List<ProductItem>> getTopSellingProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getTopSellingItems());
}

}

@Singleton(as: ProductItemProvider)
class RealProductItemProvider implements ProductItemProvider {
const RealProductItemProvider();

@override
Future<List<ProductItem>> getTrendingProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getTrendingItems());
}

@override
Future<List<ProductItem>> getFeaturedProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getFeaturedItems());
}

@override
Future<List<ProductItem>> getTopSellingProducts() async{
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
return Future.value(MockUtil.getTopSellingItems());
}

}

It consists of two different implementations one for mock data and another for the real API calls such that we can easily switch between them when required.

Add a “constant/enum.dart” file with the following contents to represent different types of product lists.

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

enum ITEM_TYPE{
featured,
topSelling,
trending
}

Create the “ui/common/item/data/repository/product_item_repository.dart” file with the following contents.

import 'package:flutter_shopping_app/constant/enum.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/ui/common/item/data/provider/product_item_provider.dart';
import 'package:injectable/injectable.dart';

@injectable
class ProductItemRepository {
final ProductItemProvider _provider;

const ProductItemRepository({@Named("mock") ProductItemProvider provider})
: _provider = provider;

Future<List<ProductItem>> getProductList({ITEM_TYPE type}) async {
return type == ITEM_TYPE.featured
? _provider.getFeaturedProducts()
: type == ITEM_TYPE.top_selling
? _provider.getTopSellingProducts()
: type == ITEM_TYPE.trending
? _provider.getTrendingProducts()
: Future.value(null);
}
}

Here @Named(“mock”) ProductItemProvider provider will provide mocked implementation while the ProductItemProvider provider will inject the other one without the named annotation. Now run the command “fvm flutter packages pub run build_runner build” to generate the necessary changes.

Note: We must run the command “fvm flutter packages pub run build_runner build” whenever we make changes to the injector configurations or add new data providers with injectable annotations. Add the additional flag — delete-conflicting-outputs if you run into the error. So the full code becomes “fvm flutter packages pub run build_runner build — delete-conflicting-outputs”

Now we’ll be fetching the product list data from the state management technique — flutter_bloc.

Create the “ui/common/item/bloc/product_list_cubit.dart” file with the following contents.

import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_shopping_app/constant/enum.dart';
import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/ui/common/item/data/repository/product_item_repository.dart';
import 'package:injectable/injectable.dart';

part 'product_item_state.dart';

@injectable
class ProductItemCubit extends Cubit<ProductItemState> {
final ProductItemRepository _productRepository;

ProductItemCubit(ProductItemRepository repository,
{@factoryParam ProductItem product})
: _productRepository = repository,
super(ProductItemState.initial());

void loadProducts({ITEM_TYPE type}) async {
try {
emit(state.update(loading: true));

List<ProductItem> products =
await _productRepository.getProductList(type: type);

emit(state.update(loading: false, items: products, init: true));
} catch (e) {
final errorMsg = e.toString();
emit(state.update(loading: false, error: errorMsg));
}
}
}

And also the state file “ui/common/item/bloc/product_list_state.dart” with the following contents:

part of 'product_item_cubit.dart';

@immutable
class ProductItemState extends Equatable {
final bool loading;
final bool init;
final String error;

final List<ProductItem> items;

bool get hasError => error != null && error.isNotEmpty;

bool get hasData => init && items.isNotEmpty;

bool get hasNoData => init && items.isEmpty;

int get size => items?.length ?? 0;

const ProductItemState({this.loading, this.error, this.init, this.items});

@override
List<Object> get props => [loading, error, init, error];

factory ProductItemState.initial({loading}) {
return ProductItemState(
loading: loading ?? true, error: "", init: false, items: []);
}

ProductItemState update({bool loading, bool init, items, error}) {
return ProductItemState(
loading: loading ?? this.loading,
items: items ?? this.items,
init: init ?? this.init,
error: error ?? "",
);
}

@override
String toString() =>
'ProductItemState { loading: $loading, error: $error, init: $init }';
}

We have different variables for storing different states: loading state, error state, initialized state, and also state to store the fetched items.

Dont forget to run “fvm flutter packages pub run build_runner build” command since we have added new instance with @injectable annotation.

Then make changes to the “item_list.dart” file as:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_shopping_app/constant/enum.dart';
import 'package:flutter_shopping_app/ui/common/item/bloc/product_item_cubit.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';
import 'package:get_it/get_it.dart';

class ItemList extends StatelessWidget {
final ITEM_TYPE type;
final String title;
final double topMargin;

const ItemList(this.type, {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,
),
],
),
),
BlocProvider<ProductItemCubit>(
create: (context) =>
GetIt.instance.get<ProductItemCubit>()..loadProducts(type: type),
child: SizedBox(
height: 158,
child: BlocBuilder<ProductItemCubit, ProductItemState>(
builder: (context, state) {
return ListView.builder(
itemBuilder: (context, index) => ItemCard(
item: state.items[index],
),
itemCount: state.items.length,
scrollDirection: Axis.horizontal,
);
}),
),
),
],
);
}
}

Did you feel that something is missing in this implementation? Yes, you got it right, widgets show empty elements when in the loading state. Similarly, we also have to handle the error state where the user should get the option to retry and also provide the user with the appropriate error message.

We’ll design two cards one for indicating the loading state and another for showing error cases where the users will also have the option to retry.

We’ll also design the card for showing when there will be no items in the particular list.

Create the file “ui/common/item/item_loader.dart” with the following contents:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_shopping_app/constant/color.dart';
import 'package:flutter_shopping_app/constant/enum.dart';
import 'package:flutter_shopping_app/ui/common/item/bloc/product_item_cubit.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';
import 'package:get_it/get_it.dart';

class ItemLoaderCard extends StatelessWidget {
final String title;
final double topMargin;
final String errorMsg;
final VoidCallback retry;

bool get _hasError => errorMsg != null && errorMsg.isNotEmpty;

const ItemLoaderCard(
{Key key, this.title: "", this.topMargin: 10, this.errorMsg, this.retry})
: super(key: key);

@override
Widget build(BuildContext context) {
return Container(
width: 120,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
offset: Offset(0, 1), // changes position of shadow
),
], borderRadius: BorderRadius.circular(10), color: Colors.white),
child: _hasError ? _errorContainer() : _loaderContainer(),
);
}

_loaderContainer() {
return Column(
children: [
Container(
height: 100,
color: ThemeTextColorLightest,
),
SizedBox(
height: 4,
),
Container(
height: 14,
color: ThemeTextColorLightest,
),
SizedBox(
height: 4,
),
Row(
children: [
Expanded(
child: Container(
height: 12,
color: ThemeTextColorLightest,
),
),
SizedBox(
width: 4,
),
Expanded(
child: Container(
height: 12,
color: ThemeTextColorLightest,
),
),
],
)
],
);
}

_errorContainer() {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
errorMsg,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: Colors.red),
),
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
backgroundColor: MaterialStateProperty.all<Color>(Colors.red)),
onPressed: retry,
child: Text('Retry'),
)
],
);
}
}

class EmptyCard extends StatelessWidget {
final String message;
final EdgeInsets margin;

const EmptyCard(
{Key key,
this.message,
this.margin: const EdgeInsets.symmetric(horizontal: 10)})
: super(key: key);

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.center,
margin: margin,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: ThemeTextColorLighter,
width: 1,
)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Icon(Icons.clear),
SizedBox(
width: 10,
),
Expanded(child: Text(message ?? "No data",))
],
));
}
}

And make the changes in “ui/common/item/item_list.dart” to implement the cards we have designed above.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_shopping_app/constant/enum.dart';
import 'package:flutter_shopping_app/ui/common/item/bloc/product_item_cubit.dart';
import 'package:flutter_shopping_app/ui/common/item/item_card.dart';
import 'package:flutter_shopping_app/ui/common/item/item_loader.dart';
import 'package:get_it/get_it.dart';

class ItemList extends StatelessWidget {
final ITEM_TYPE type;
final String title;
final double topMargin;

const ItemList(this.type, {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,
),
],
),
),
BlocProvider<ProductItemCubit>(
create: (context) =>
GetIt.instance.get<ProductItemCubit>()..loadProducts(type: type),
child: SizedBox(
height: 158,
child: BlocBuilder<ProductItemCubit, ProductItemState>(
builder: (context, state) {
if (state.hasNoData) return EmptyCard();
return ListView.builder(
itemBuilder: (context, index) => index >= state.items.length
? ItemLoaderCard(
errorMsg: state.error,
retry: () => _retry(context),
)
: ItemCard(
item: state.items[index],
),
itemCount: !state.init
? 4
: state.hasError
? state.items.length + 1
: state.loading
? state.items.length + 2
: state.items.length,
scrollDirection: Axis.horizontal,
);
}),
),
),
],
);
}

_retry(BuildContext context) {
context.read<ProductItemCubit>().loadProducts(type: type);
}
}

Next, we’ll work on the new page which will show the details of the item being clicked from where the user also will be able to add that particular item to their cart.

Add some fields(description, rating, and id) in the product_item model:

...
final double rating;
final String description;
final int id;
final List<String> benefits;

bool get hasBenefits => benefits.isNotEmpty;
ProductItem({
this.name,
this.rating,
this.description,
this.id,
this.imageUrl,
this.currency,
this.currencyType: "\$",
this.discount,
this.sellingUnit,
});
...

Then run the command “fvm flutter packages pub run build_runner build — delete-conflicting-outputs”

Change the following in the MockUtil class:

....static Map getTrendingItems() {
return {
"data": [
{
"name": "Cabbages",
"imageUrl":
"https://creazilla-store.fra1.digitaloceanspaces.com/cliparts/1502591/cabbage-clipart-md.png",
"currency": 9.9,
"currencyType": "\$",
"sellingUnit": "per piece",
"discount": 10,
"rating": 4.5,
"description":
"Cabbage (comprising several cultivars of Brassica oleracea) is a leafy green, red (purple), or white (pale green) biennial plant grown as an annual vegetable crop for its dense-leaved heads. It is descended from the wild cabbage (B. oleracea var. oleracea), and belongs to the \"cole crops\" or brassicas, meaning it is closely related to broccoli and cauliflower (var. botrytis); Brussels sprouts (var. gemmifera); and Savoy cabbage (var. sabauda).",
"id": 1,
"benefits": [
"Cabbage Is Packed With Nutrients.",
"It May Help Keep Inflammation in Check. Inflammation isn’t always a bad thing. In fact, your body relies on the inflammatory response to protect against infection or speed up healing. This kind of acute inflammation is a normal response to an injury or infection.",
"Cabbage Is Packed With Vitamin C.",
"It Helps Improve Digestion. Cabbage contains insoluble fiber, which keeps the digestive system healthy by providing fuel for friendly bacteria and promoting regular bowel movements.",
"Cabbage contains powerful pigments called anthocyanins, which have been shown to reduce the risk of heart disease.",
"Potassium helps keep blood pressure within a healthy range. Increasing your intake of potassium-rich foods like cabbage may help lower high blood pressure levels.",
"Cabbage is a good source of soluble fiber and plant sterols. These substances have been shown to reduce LDL cholesterol."
]
},
{
"name": "Tomatoes",
"imageUrl": "https://pngimg.com/uploads/tomato/tomato_PNG12594.png",
"currency": 7.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id": 2,
"rating": 3.5,
"description":
"The tomato is the edible berry of the plant Solanum lycopersicum,[1][2] commonly known as a tomato plant. The species originated in western South America and Central America.[2][3] The Nahuatl (the language used by the Aztecs) word tomatl gave rise to the Spanish word tomate, from which the English word tomato derived.[3][4] Its domestication and use as a cultivated food may have originated with the indigenous peoples of Mexico.[2][5] The Aztecs used tomatoes in their cooking at the time of the Spanish conquest of the Aztec Empire, and after the Spanish encountered the tomato for the first time after their contact with the Aztecs, they brought the plant to Europe. From there, the tomato was introduced to other parts of the European-colonized world during the 16th century"
},
{
"name": "Potatoes",
"imageUrl":
"https://www.freeiconspng.com/uploads/slice-of-potato-png-5.png",
"currency": 9.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id": 3,
"rating": 4,
"description":
"Potatoes are edible tubers, available worldwide and all year long. They are relatively cheap to grow, rich in nutrients, and they can make a delicious treat."
},
{
"name": "Strawberries",
"imageUrl":
"https://www.freeiconspng.com/uploads/strawberry-png-9.png",
"currency": 9.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id": 4,
"rating": 3.5,
"description":
"The garden strawberry (or simply strawberry; Fragaria × ananassa)[1] is a widely grown hybrid species of the genus Fragaria, collectively known as the strawberries, which are cultivated worldwide for their fruit. The fruit is widely appreciated for its characteristic aroma, bright red color, juicy texture, and sweetness. It is consumed in large quantities, either fresh or in such prepared foods as jam, juice, pies, ice cream, milkshakes, and chocolates. Artificial strawberry flavorings and aromas are also widely used in products such as candy, soap, lip gloss, perfume, and many others."
}
]
};
}

static Map getFeaturedItems() {
return {
"data": [
{
"name": "Carrots",
"imageUrl":
"https://www.transparentpng.com/thumb/carrot/71HwEm-fresh-carrot-photos.png",
"currency": 19.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id": 5,
"rating": 1.5,
"description":
"The carrot (Daucus carota) is a root vegetable often claimed to be the perfect health food."
},
{
"name": "Banana",
"imageUrl":
"https://www.pngkey.com/png/full/1009-10099133_banana-png-free-commercial-use-image-.png",
"currency": 7.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id:": 6,
"rating": 2.5,
"description":
"Bananas are one of the world's most appealing fruits. Global banana exports reached about 18 million tons in 2015, according to the United Nations. About half of them went to the United States and the European market. In the United States, each person eats 11.4 lbs. of bananas per year, according to the U.S. Department of Agriculture, making it Americans' favorite fresh fruit."
},
{
"name": "Beef",
"imageUrl":
"https://www.pngkey.com/png/full/36-368340_beef-transparent-background.png",
"currency": 49.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 10,
"id": 7,
"rating": 4.5,
"description":
"Beef is categorized as red meat — a term used for the meat of mammals, which contains higher amounts of iron than chicken or fish."
},
{
"name": "Pineapple",
"imageUrl":
"https://www.pngkey.com/png/full/6-67795_free-png-pineapple-png-images-transparent-pineapple-png.png",
"currency": 7,
"currencyType": "\$",
"sellingUnit": "per piece",
"discount": 5,
"id": 8,
"rating": 4.5,
"description":
"Pineapples are tropical fruits that are rich in vitamins, enzymes and antioxidants. They may help boost the immune system, build strong bones and aid indigestion. And, despite their sweetness, pineapples are low in calories."
}
]
};
}

static Map getTopSellingItems() {
return {
"data": [
{
"name": "Peas",
"imageUrl": "http://pngimg.com/uploads/pea/small/pea_PNG24285.png",
"currency": 17.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 7,
"id": 9,
"rating": 4.5,
"description":
"The pea is most commonly the small spherical seed or the seed-pod of the pod fruit Pisum sativum. Each pod contains several peas, which can be green or yellow. Botanically, pea pods are fruit,[2] since they contain seeds and develop from the ovary of a (pea) flower. The name is also used to describe other edible seeds from the Fabaceae such as the pigeon pea (Cajanus cajan), the cowpea (Vigna unguiculata), and the seeds from several species of Lathyrus."
},
{
"name": "Chips",
"imageUrl":
"http://www.pngall.com/wp-content/uploads/4/Potato-Chips-PNG-Clipart.png",
"currency": 7.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 12,
"id": 10,
"rating": 4.5,
"description":
"A chip (American English and Australian English) or crisp (British English) is a snack food in the form of a crisp, flat or slightly bowl shaped, bite-sized unit. Chips are often served in a combination of chips and dip."
},
{
"name": "Corn",
"imageUrl": "https://freepngimg.com/thumb/categories/951.png",
"currency": 49.9,
"currencyType": "\$",
"sellingUnit": "per kg",
"discount": 15,
"id": 11,
"rating": 4.5,
"description":
"Corn, (Zea mays), also called Indian corn or maize, cereal plant of the grass family (Poaceae) and its edible grain. The domesticated crop originated in the Americas and is one of the most widely distributed of the world’s food crops. Corn is used as livestock feed, as human food, as biofuel, and as raw material in industry. In the United States the colourful variegated strains known as Indian corn are traditionally used in autumn harvest decorations."
},
{
"name": "Spinach",
"imageUrl":
"https://dehatisabji.com/wp-content/uploads/2020/11/Spinach-All-Green-600x469-1.jpg",
"currency": 7,
"currencyType": "\$",
"sellingUnit": "per piece",
"discount": 5,
"id": 12,
"rating": 4.5,
"description":
"Spinach is a superfood. It is loaded with tons of nutrients in a low-calorie package. Dark, leafy greens like spinach are important for skin, hair, and bone health. They also provide protein, iron, vitamins, and minerals.",
"benefits": [
"Diabetes management: Spinach contains an antioxidant known as alpha-lipoic acid, which has been shown to lower glucose levels, increase insulin sensitivity, and prevent oxidative, stress-induced changes in patients with diabetes.",
"Cancer prevention: Spinach and other green vegetables contain chlorophyll. Several studies, including this 2013 study carried out on 12,000 animals, have shown chlorophyll to be effective at blockingTrusted Source the carcinogenic effects of heterocyclic amines.",
"Asthma prevention: A study of 433 children with asthma between the ages of 6 and 18 years, and 537 children without, showed that the risks for developing asthma are lowerTrusted Source in people who have a high intake of certain nutrients.",
"Lowering blood pressure: Due to its high potassium content, spinach is recommended for people with high blood pressure.",
"Bone health: Low intakes of vitamin K have been associated with a higher risk of bone fracture.",
"Promotes digestive regularity: Spinach is high in fiber and water, both of which help to prevent constipation and promote a healthy digestive tract.",
]
}
]
};
}
....

And the contents in “lib/ui/common/item/data/provider/product_item_provider.dart” as:

import 'package:flutter_shopping_app/ui/common/item/data/model/product_item.dart';
import 'package:flutter_shopping_app/util/mock_util.dart';
import 'package:injectable/injectable.dart';

abstract class ProductItemProvider {
Future<List<ProductItem>> getTrendingProducts();

Future<List<ProductItem>> getTopSellingProducts();

Future<List<ProductItem>> getFeaturedProducts();
}

@Named("mock")
@Singleton(as: ProductItemProvider)
class MockProductItemProvider implements ProductItemProvider {
const MockProductItemProvider();

@override
Future<List<ProductItem>> getTrendingProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getTrendingItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}

@override
Future<List<ProductItem>> getFeaturedProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getFeaturedItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}

@override
Future<List<ProductItem>> getTopSellingProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getTopSellingItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}
}

@Singleton(as: ProductItemProvider)
class RealProductItemProvider implements ProductItemProvider {
const RealProductItemProvider();

@override
Future<List<ProductItem>> getTrendingProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getTrendingItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}

@override
Future<List<ProductItem>> getFeaturedProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getFeaturedItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}

@override
Future<List<ProductItem>> getTopSellingProducts() async {
//add some delay to give the feel of api call
await Future.delayed(Duration(seconds: 3));
final Map _data = MockUtil.getTopSellingItems();
return Future.value(
(_data['data'] as List)?.map((e) => ProductItem.fromJson(e))?.toList());
}
}

Add the following code for a custom button in the file “lib/ui/common/form/custom_button.dart”:

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

class CustomButton extends StatelessWidget {
final String label;
final Widget child;
final EdgeInsets padding;
final Color backgroundColor;
final Color disabledColor;
final VoidCallback onPressed;
final bool disabled;

bool get _disabled => disabled || onPressed == null;

const CustomButton(
{Key key,
this.label,
this.onPressed,
this.disabled: false,
this.child,
this.backgroundColor: ThemeColor,
this.disabledColor: ThemeTextColorLightest,
this.padding: const EdgeInsets.all(20)})
: super(key: key);

@override
Widget build(BuildContext context) {
return TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
// minimumSize: Size(88, 44),
padding: padding,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
backgroundColor: _disabled ? disabledColor : backgroundColor,
),
onPressed: _disabled ? null : onPressed,
child: child ??
Text(
label ?? "",
style: TextStyle(color: Colors.white),
),
);
}
}

Also, add code for the incrementer widget in the file “lib/ui/common/incrementer.dart”.

import 'dart:math';

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

class Incrementer extends StatelessWidget {
const Incrementer({
Key key,
this.setIncrementer,
this.value: 1,
this.maxVal: 50,
this.minVal: 1,
}) : super(key: key);

final int minVal;
final int maxVal;
final int value;
final Function(int) setIncrementer;

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: ThemeTextColorLighter),
borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_incrementerWidget(add: false),
SizedBox(
width: 42,
child: Text(
this.value.toString(),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
)),
_incrementerWidget()
],
),
);
}

Widget _incrementerWidget({add: true}) {
return IconButton(
onPressed: () {
if (((add && value + 1 <= maxVal) || (!add && value - 1 >= minVal)) &&
setIncrementer != null)
setIncrementer(
add ? min(value + 1, maxVal) : max(value - 1, minVal));
},
color: Colors.black,
icon: Icon(add ? Icons.add : Icons.remove));
}
}

Add an entry flutter_rating_bar: ^4.0.0 in the dependency section in pubspec.yaml.

Create the file “lib/ui/common/item/detail/item_detail.dart” which is the new page to show the details of the selected product with the following contents.

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

class ItemDetail extends StatefulWidget {
final ProductItem item;

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

@override
_ItemDetailState createState() => _ItemDetailState();
}

class _ItemDetailState extends State<ItemDetail> {
int _noOfItems = 1;

@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
child: Scaffold(
body: Stack(
children: [
ListView(
padding: EdgeInsets.fromLTRB(20, 10, 20, 0),
children: [
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(ThemeColor)),
onPressed: () => _goBack(context),
icon: Icon(Icons.keyboard_arrow_left),
label: Text("Back")),
),
SizedBox(
height: 30,
),
Image.network(
widget.item.imageUrl,
height: 300,
),
SizedBox(
height: 30,
),
Text(
widget.item.name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
),
SizedBox(
height: 10,
),
RatingBar.builder(
initialRating: min(widget.item.rating ?? 0, 5),
minRating: 1,
itemSize: 20,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemBuilder: (context, _) =>
Icon(Icons.star, color: Colors.amber),
),
SizedBox(
height: 30,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Incrementer(
setIncrementer: _updateCartCount,
value: _noOfItems,
),
Text(
widget.item.price,
style: TextStyle(fontSize: 30),
)
],
),
SizedBox(
height: 30,
),
Text("About the Product",
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(
height: 15,
),
Text(
widget.item.description ?? "",
style: TextStyle(
fontSize: 14,
color: ThemeTextColor,
),
textAlign: TextAlign.justify,
),
if (widget.item.hasBenefits) _addItemBenefits(),
SizedBox(
height: 70,
)
],
),
Align(
child: _addToCartWidget(),
alignment: Alignment.bottomCenter,
)
],
),
),
),
);
}

Widget _addToCartWidget() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(width: 1.0, color: ThemeTextColorLighter))),
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(icon: Icon(Icons.favorite_border)),
CustomButton(
label: "Add to cart",
padding: EdgeInsets.symmetric(
vertical: 15,
horizontal: 60,
),
),
],
),
);
}

Widget _addItemBenefits() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 20,
),
Text("Benefits",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
SizedBox(
height: 10,
),
...List.generate(
widget.item.benefits.length,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.arrow_forward,
size: 16,
),
SizedBox(
width: 10,
),
Expanded(
child: Text(
widget.item.benefits[index],
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: ThemeTextColor),
))
],
),
),
),
],
);
}

_goBack(BuildContext context) {
Navigator.of(context).pop();
}

_updateCartCount(int count) {
setState(() {
_noOfItems = count;
});
}
}

Finally, add the trigger in the file “lib/ui/common/item/item_card.dart” to navigate the clicked item's details page.

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

_goToDetailPage(context) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ItemDetail(item)),
);
}

@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _goToDetailPage(context),
hoverColor: Colors.transparent,
child: Container(
width: 120,
...

The figure above shows the final form of our application for the second part of the tutorial. Here, when the user clicks the item card, the corresponding details are shown on another page where they also can add the products to their cart or favorites.

Adding the items to the cart or favorites requires the users to be logged in which is the next thing we’ll be working on. In the next part, we will try to develop the authentication module where the user will be able to log in, so please check it out. But before that, you can also check another tutorial on Authentication, giving you a clear idea about what we’re going to do in our next tutorial.

Project Starter Link: https://github.com/cshanjib/flutter_shopping_app/tree/section2-start
Project Completed Link: https://github.com/cshanjib/flutter_shopping_app/tree/section2-end

Prev — Part 1: Designing the E-commerce App
Next —Part 3: Authentication and Login Design using the flutter_bloc 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.)

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_get_it

--

--