A complete Shopping App using Flutter-Part 4

Sanjib Maharjan
12 min readJun 27, 2023

--

This is the fourth part of the shopping app tutorial and in this part, we’ll be working on some tips for responsive design which we’ll try to implement on our shopping application. If you haven't gone through the previous 3 tutorials, it is recommended to go through them first to sync about what we’re building. And also check the tutorial on flutter responsive design to get some tips on ways of implementing responsiveness in our existing design.

The number of mobile users has increased by a great fold in this generation along with different types of mobile devices. If we build designs separately for each screen size it would be very costly and practically difficult timewise. Also knowing the audience plays an important role. If you are sure that only mobile users are using the app, it makes no sense to build for tablets or the web. But in real life, this is not the case and we cannot target only mobile users. So the best practice would be designing for at least 3 types of screen sizes which can be increased or even decreased according to the requirements.

Here in our project, we’ll be designing for three screen sizes:

const double WIDTH_MOBILE = 600;
const double WIDTH_TABLET = 900;
const double WIDTH_DESKTOP = 1024;

Also, the width of the devices that the app runs may range from small to very large screen sizes, and hence it is recommended to limit the width to the maximum width.

const double WIDTH_MAX_APP_WIDTH = 1200;

Add the file “constant/dimension.dart” with the following contents.

//screen sizes
const double WIDTH_MOBILE = 600;
const double WIDTH_TABLET = 900;
const double WIDTH_DESKTOP = 1024;

const double WIDTH_MAX_APP_WIDTH = 1200;

//Font Sizes
const double FONT_LARGE = 20;
const double FONT_NORMAL = 18;
const double FONT_SMALL = 16;
const double FONT_X_SMALL = 14;
const double FONT_XX_SMALL = 12;
const double FONT_XXX_SMALL = 10;

//padding or margins
const double GAP_XXX_SMALL = 2;
const double GAP_XX_SMALL = 4;
const double GAP_X_SMALL = 6;
const double GAP_SMALL = 8;
const double GAP_XXX_NORMAL = 10;
const double GAP_XX_NORMAL = 12;
const double GAP_X_NORMAL = 14;
const double GAP_NORMAL = 20;
const double GAP_LARGE = 24;
const double GAP_LARGER = 32;
const double GAP_XL_LARGE = 48;

//border radius
const double BORDER_RADIUS_X_SMALL = 6;
const double BORDER_RADIUS_SMALL = 10;
const double BORDER_RADIUS_LARGE = 20;
const double BORDER_RADIUS_X_LARGE = 30;

Add the file “ui/common/base/app_wrapper.dart” which is wrapper class to limit the width of our app to the max allowable width.

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

class AppWrapper extends StatelessWidget {
final Widget child;

const AppWrapper({Key key, this.child}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: WIDTH_MAX_APP_WIDTH),
child: child,
),
);
}
}

MediaQuery and LayoutBuilder

We’ll be using MediaQuery or LayoutBuilder wherever required, which gives the device width, and with that, we’ll decide the designs for the mobile, tablet, and web.

Create the file “helper/responsive_helper.dart” which is the helper class created using MediaQuery and LayoutBuilder.

import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_shopping_app/constant/dimension.dart';
typedef ResponsiveWidgetBuilder = Widget Function(ResponsiveHelper helper);

class ResponsiveBuilder extends StatelessWidget {
final ResponsiveWidgetBuilder builder;
final BuildContext context;
final Size size;

const ResponsiveBuilder({this.builder, this.size, this.context}) : super();
@override
Widget build(BuildContext context) {
return builder(ResponsiveHelper(size: size, context: context));
}
}
class ResponsiveHelper {
final Size deviceSize;

ResponsiveHelper({BuildContext context, Size size})
: assert(context != null || size != null),
deviceSize = size ?? MediaQuery.of(context).size;

bool get isDesktop => deviceSize.width >= WIDTH_DESKTOP;

double get optimalDeviceWidth => min(deviceSize.width, WIDTH_MAX_APP_WIDTH);

bool isTablet({includeMobile: false}) =>
deviceSize.width < WIDTH_DESKTOP &&
(deviceSize.width > WIDTH_MOBILE || includeMobile);

T value<T>({
@required T mobile,
T tablet,
@required T desktop,
}) =>
isMobile
? mobile
: isDesktop
? desktop
: (tablet ?? mobile);

double incremental(double mobile, {double increment = 2}) => isMobile
? mobile
: isDesktop
? mobile + (2 * increment)
: mobile + increment;

bool get isMobile => deviceSize.width <= WIDTH_MOBILE;

double get defaultSmallGap => isDesktop
? GAP_XXX_NORMAL
: isMobile
? GAP_X_SMALL
: GAP_SMALL;

double get defaultGap => isDesktop
? GAP_NORMAL
: isMobile
? GAP_XXX_NORMAL
: GAP_XX_NORMAL;

double get smallFontSize => isDesktop
? FONT_X_SMALL
: isMobile
? FONT_XXX_SMALL
: FONT_XX_SMALL;

double get normalFontSize => isDesktop
? FONT_SMALL
: isMobile
? FONT_XX_SMALL
: FONT_X_SMALL;

double get mediumFontSize => isDesktop
? FONT_NORMAL
: isMobile
? FONT_X_SMALL
: FONT_SMALL;

double get largeFontSize => isDesktop
? FONT_LARGE
: isMobile
? FONT_SMALL
: FONT_NORMAL;
}

class RowOrColumn extends StatelessWidget {
final bool showRow;
final bool intrinsicRow;
final List<Widget> children;
final MainAxisAlignment rowMainAxisAlignment;
final MainAxisSize rowMainAxisSize;
final CrossAxisAlignment rowCrossAxisAlignment;

final MainAxisAlignment columnMainAxisAlignment;
final MainAxisSize columnMainAxisSize;
final CrossAxisAlignment columnCrossAxisAlignment;

RowOrColumn({
this.showRow: true,
this.intrinsicRow: false,
Key key,
this.children,
this.columnMainAxisAlignment = MainAxisAlignment.start,
this.columnMainAxisSize = MainAxisSize.max,
this.columnCrossAxisAlignment = CrossAxisAlignment.center,
this.rowMainAxisAlignment = MainAxisAlignment.start,
this.rowMainAxisSize = MainAxisSize.max,
this.rowCrossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return showRow
? _intrinsicHeightWrap(
intrinsicRow,
child: Row(
children: children,
mainAxisAlignment: rowMainAxisAlignment,
crossAxisAlignment: rowCrossAxisAlignment,
mainAxisSize: rowMainAxisSize,
),
)
: Column(
children: children,
mainAxisAlignment: columnMainAxisAlignment,
crossAxisAlignment: columnCrossAxisAlignment,
mainAxisSize: columnMainAxisSize,
);
}

Widget _intrinsicHeightWrap(bool wrap, {child}) {
return wrap
? IntrinsicHeight(
child: child,
)
: child;
}
}

class ExpandedIf extends StatelessWidget {
final bool expanded;
final Widget child;
final int flex;

ExpandedIf({this.expanded: true, this.child, Key key, this.flex: 1})
: super(key: key);

@override
Widget build(BuildContext context) {
return expanded
? Expanded(
child: child,
flex: flex,
)
: child;
}
}

class MouseRegionIf extends StatelessWidget {
final bool addRegion;
final Widget child;

final PointerExitEventListener onExit;
final PointerEnterEventListener onEnter;
final PointerHoverEventListener onHover;
final MouseCursor cursor;

MouseRegionIf(
{this.addRegion: true,
this.child,
this.onExit,
this.onEnter,
this.onHover,
this.cursor,
Key key})
: super(key: key);

@override
Widget build(BuildContext context) {
return addRegion
? MouseRegion(
cursor: cursor,
onEnter: onEnter,
onExit: onExit,
onHover: onHover,
child: child,
)
: child;
}
}

class PaddingSwitch extends StatelessWidget {
final bool switchIf;
final Widget child;
final EdgeInsets padding;
final EdgeInsets switchedPadding;

PaddingSwitch(
{this.switchIf: false,
this.child,
this.padding: EdgeInsets.zero,
this.switchedPadding: EdgeInsets.zero,
Key key})
: super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
child: child,
padding: switchIf ? switchedPadding : padding,
);
}
}

class ResponsiveWidget extends StatelessWidget {
final Widget mobile;
final Widget tablet;
final Widget desktop;

const ResponsiveWidget({
Key key,
@required this.mobile,
this.tablet,
@required this.desktop,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= WIDTH_DESKTOP) {
return desktop;
} else if (constraints.maxWidth > WIDTH_MOBILE) {
return tablet ?? mobile;
} else {
return mobile;
}
},
);
}
}

First, we’ll proceed to make the login popup responsive by using the above helper classes. We will also try to make the UI flexible wherever possible by using the classes such as Flexible, Expanded, etc.

Make the following changes to “ui/auth/login/login.dart” file.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_shopping_app/bloc/token/auth_token_cubit.dart';
import 'package:flutter_shopping_app/constant/color.dart';
import 'package:flutter_shopping_app/constant/dimension.dart';
import 'package:flutter_shopping_app/ui/auth/login/bloc/login_cubit.dart';
import 'package:flutter_shopping_app/ui/common/base/responsive_helper.dart';
import 'package:flutter_shopping_app/ui/common/form/custom_button.dart';
import 'package:flutter_shopping_app/ui/common/form/custom_text_field.dart';
import 'package:get_it/get_it.dart';

class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
bool _isVisible = false;

final _userData = {"username": "", "password": ""};

@override
void initState() {
_usernameController
.addListener(() => _setValue("username", _usernameController));
_passwordController
.addListener(() => _setValue("password", _passwordController));
super.initState();
}

@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}

_setValue(key, TextEditingController controller) {
if (_userData[key] != controller.text.trim()) {
setState(() {
_userData[key] = controller.text.trim();
});
}
}

_listenEvents(BuildContext context, LoginState state) {
if (state.success) {
_close(context);
}
}

@override
Widget build(BuildContext context) {
final ResponsiveHelper _responsiveHelper =
ResponsiveHelper(context: context);
return BlocProvider<LoginCubit>(
create: (context) =>
GetIt.I.get<LoginCubit>(param1: context.read<AuthTokenCubit>()),
child: BlocListener<LoginCubit, LoginState>(
listener: _listenEvents,
child: BlocBuilder<LoginCubit, LoginState>(builder: (context, state) {
return Stack(
children: [
Container(
constraints: BoxConstraints(maxWidth: WIDTH_TABLET),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_AppLoginTitle(),
RowOrColumn(
intrinsicRow: true,
showRow: !_responsiveHelper.isMobile,
children: [
ExpandedIf(
expanded: !_responsiveHelper.isMobile,
child: _AppIcon(_responsiveHelper),
flex: 2,
),
ExpandedIf(
expanded: !_responsiveHelper.isMobile,
child: _LoginForm(
usernameController: _usernameController,
passwordController: _passwordController,
isPasswordVisible: _isVisible,
state: state,
toggleVisibility: _toggleVisibility,
userData: _userData,
),
flex: 3,
)
],
),
],
),
),
),
Positioned(
child: IconButton(
icon: Icon(Icons.close), onPressed: () => _close(context)),
top: 10,
right: 10,
)
],
);
}),
),
);
}

_toggleVisibility() {
setState(() {
_isVisible = !_isVisible;
});
}

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

class _LoginForm extends StatelessWidget {
final TextEditingController usernameController;
final TextEditingController passwordController;
final LoginState state;
final bool isPasswordVisible;
final VoidCallback toggleVisibility;
final Map userData;

const _LoginForm(
{Key key,
this.usernameController,
this.passwordController,
this.state,
this.isPasswordVisible,
this.toggleVisibility,
this.userData})
: super(key: key);

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 30,
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
child: CustomTextField(
placeholder: "Username",
enabled: !state.loading,
controller: usernameController,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: CustomTextField(
placeholder: "Password",
enabled: !state.loading,
controller: passwordController,
obscureText: !isPasswordVisible,
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
color: ThemeColor,
),
onPressed: toggleVisibility,
),
),
),
CustomButton(
label: "Login",
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 70),
onPressed: () => _login(context),
disabled: state.loading || !_isUsernameValid() || !_isPasswordValid(),
),
SizedBox(
height: 20,
),
Text(
"Don't have an account? Sign up",
style: TextStyle(color: ThemeColor),
),
SizedBox(
height: 10,
),
Text(
"Forgot password?",
style: TextStyle(color: ThemeTextColorLight),
),
SizedBox(
height: 30,
),
],
);
}

_login(BuildContext context) {
context
.read<LoginCubit>()
.login(userData["username"], userData["password"]);
}

bool _isUsernameValid() {
return userData["username"].isNotEmpty;
}

bool _isPasswordValid() {
return userData["password"].isNotEmpty;
}
}

class _AppIcon extends StatelessWidget {
final ResponsiveHelper _responsiveHelper;

const _AppIcon(ResponsiveHelper responsiveHelper, {Key key})
: _responsiveHelper = responsiveHelper,
super(key: key);

@override
Widget build(BuildContext context) {
return _clipBackground(
clip: !_responsiveHelper.isMobile,
child: Column(
children: [
SizedBox(
height: 20,
),
_bagHandle(),
Container(
color: ThemeColorLighter,
height: 150,
width: 150,
child: TextButton.icon(
icon: Icon(
Icons.login_rounded,
size: 50,
color: ThemeColor,
),
label: Text(
"SHOP",
style: TextStyle(fontSize: 20),
)),
),
],
),
);
}

_clipBackground({Widget child, clip: false}) {
return clip
? ClipPath(
clipper: MyCustomClipper(),
child: Container(
child: child,
decoration: BoxDecoration(
color: ThemeTextColorLightest,
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(20))),
padding: EdgeInsets.symmetric(vertical: 100),
),
)
: Align(
child: child,
);
}

_bagHandle(
{double height: 40,
double handleWidth: 10,
handleColor: ThemeColor,
bgColor: ThemeTextColorLightest}) {
return Container(
decoration: BoxDecoration(
color: handleColor,
borderRadius:
BorderRadius.vertical(top: Radius.circular(height / 2))),
child: Container(
margin: EdgeInsets.fromLTRB(handleWidth, handleWidth, handleWidth, 0),
height: height,
width: height * 2,
decoration: BoxDecoration(
color: bgColor,
borderRadius:
BorderRadius.vertical(top: Radius.circular(height / 2))),
),
);
}
}

class _AppLoginTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 40, top: 20),
child: Text(
"Login to the app",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: ThemeTextColorLight),
),
),
Divider(
height: 0,
thickness: 16,
color: ThemeTextColorLightest,
)
],
);
}
}

class MyCustomClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
Path path = Path()
..lineTo(size.width, 0)
..quadraticBezierTo(
size.width / 1.2, size.height / 2, size.width, size.height)
..lineTo(size.width, size.height) // Add line p2p3
..lineTo(0, size.height) // Add line p2p3
..close();
return path;
}

@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

Instantiate the ResponsiveHelper class as:

final ResponsiveHelper _respHelper = ResponsiveHelper(context: context);

Generate dynamic values for mobile, tablet, and desktop by the following code.

_respHelper.value<double>(mobile: 80, tablet: 120, desktop: 160);

Check for different device types and render different components accordingly.

if(_respHelper.isMobile){
//mobile related logic
}else if(_respHelper.isDesktop){
//desktop related logic
}
Showing Responsive Login UI for mobile, iPad, and desktop view.

Remove the tag for debug mode by adding “debugShowCheckedModeBanner: false” in MaterialApp class in the “main.dart” file.

Create the “ui/common/top_title.dart” file with the following content.

import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/ui/common/base/responsive_helper.dart';

class TopTitle extends StatelessWidget {
final String title;
final double topMargin;
final VoidCallback onPressed;
final ResponsiveHelper responsiveHelper;

const TopTitle(this.responsiveHelper,
{Key key, this.title, this.topMargin, this.onPressed})
: super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(
responsiveHelper.defaultGap,
topMargin ?? responsiveHelper.defaultGap,
responsiveHelper.defaultGap,
responsiveHelper.defaultSmallGap),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontSize: responsiveHelper.mediumFontSize,
fontWeight: FontWeight.bold),
),
InkWell(
child: Text("see all",
style: TextStyle(
fontSize: responsiveHelper.normalFontSize,
fontWeight: FontWeight.bold)),
onTap: onPressed,
),
],
),
);
}
}

Similarly, make changes to the “ui/category/category_list.dart” file 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/helper/responsive_helper.dart';
import 'package:flutter_shopping_app/ui/common/top_title.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;

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

@override
Widget build(BuildContext context) {
final List<CategoryItem> _category = MockUtil.getMockAppCategories();
final ResponsiveHelper _respHelper = ResponsiveHelper(context: context);
return Column(
children: [
TopTitle(
_respHelper,
title: title ?? "All categories",
),
SizedBox(
height:
_respHelper.value<double>(mobile: 80, tablet: 120, desktop: 160),
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,
respHelper: _respHelper,
),
),
),
],
);
}
}

Also, make the following changes to the “ui/category/category_item_card.dart” file.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/helper/responsive_helper.dart';

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

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

@override
Widget build(BuildContext context) {
final ResponsiveHelper _respHelper =
respHelper ?? ResponsiveHelper(context: context);
final double _listHeight =
_respHelper.value<double>(mobile: 80, tablet: 120, desktop: 160);
return Container(
height: _listHeight,
width: _listHeight + 20,
padding: EdgeInsets.all(_respHelper.incremental(6)),
margin: EdgeInsets.symmetric(horizontal: _respHelper.incremental(8)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Color(themeColor)),
child: Column(
children: [
Expanded(child: Image.network(imageUrl)),
SizedBox(
height: _respHelper.defaultSmallGap,
),
Text(
title ?? "Vegetables",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: _respHelper.normalFontSize,
fontWeight: FontWeight.bold),
),
],
),
);
}
}

Change the “ui/common/carousel/custom_carousel.dart” as:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_app/helper/responsive_helper.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}) : super(key: key);

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

Next, we’ll make changes in the “ui/common/item/item_card.dart” file.

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

class ItemCard extends StatelessWidget {
final ProductItem item;
final ResponsiveHelper responsiveHelper;

const ItemCard({Key key, this.item, this.responsiveHelper}) : 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: responsiveHelper.value<double>(mobile: 120, desktop: 200, tablet: 160),
padding: const EdgeInsets.all(10),
margin: EdgeInsets.symmetric(
horizontal: responsiveHelper.defaultSmallGap, 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,
border: Border.all(width: 1, color: ThemeTextColorLightest)),
child: Column(
children: [
Image.network(
item.imageUrl,
height: responsiveHelper.value<double>(mobile: 100, desktop: 140, tablet: 120),
),
SizedBox(height: responsiveHelper.incremental(4, increment: 4),),
Align(
child: Text(
item.name,
style: TextStyle(fontSize: responsiveHelper.normalFontSize, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
alignment: Alignment.centerLeft,
),
SizedBox(height: responsiveHelper.incremental(4),),
Row(
children: [
Text(item.price,
style: TextStyle(fontSize: responsiveHelper.normalFontSize, color: ThemeTextColor)),
SizedBox(
width: responsiveHelper.incremental(4),
),
Expanded(
child: Text(
item.sellingUnit,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: responsiveHelper.smallFontSize, color: ThemeTextColorLight),
),
),
],
)
],
),
),
);
}
}

Also, make the following changes to the “ui/common/item/item_loader.dart” file.

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

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

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

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

@override
Widget build(BuildContext context) {
return Container(
width: responsiveHelper.value(mobile: 120, desktop: 200, tablet: 160),
padding: const EdgeInsets.all(10),
margin: EdgeInsets.symmetric(
horizontal: responsiveHelper.defaultSmallGap, 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,
border: Border.all(width: 1, color: ThemeTextColorLightest)),
child: _hasError ? _errorContainer() : _loaderContainer(),
);
}

_loaderContainer() {
return Column(
children: [
Container(
height: responsiveHelper.value<double>(mobile: 100, desktop: 140, tablet: 120),
color: ThemeTextColorLightest,
),
SizedBox(height: responsiveHelper.incremental(4, increment: 4),),
Container(
height: 14,
color: ThemeTextColorLightest,
),
SizedBox(height: responsiveHelper.incremental(4),),
Row(
children: [
Expanded(
child: Container(
height: 12,
color: ThemeTextColorLightest,
),
),
SizedBox(height: responsiveHelper.incremental(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;
final ResponsiveHelper responsiveHelper;

const EmptyCard(
{Key key,
this.message,
this.responsiveHelper,
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",
))
],
));
}
}

Finally, make changes to the “ui/common/item/item_list.dart” file and we are done making the item list widget responsive.

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/helper/responsive_helper.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:flutter_shopping_app/ui/common/top_title.dart';
import 'package:get_it/get_it.dart';

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

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

_seeAll() {}

@override
Widget build(BuildContext context) {
final ResponsiveHelper _respHelper = ResponsiveHelper(context: context);
return Column(
children: [
TopTitle(
_respHelper,
title: title ?? "Items",
onPressed: _seeAll,
),
BlocProvider<ProductItemCubit>(
create: (context) =>
GetIt.instance.get<ProductItemCubit>()..loadProducts(type: type),
child: SizedBox(
height: _respHelper.value<double>(
mobile: 164, desktop: 222, tablet: 194),
child: BlocBuilder<ProductItemCubit, ProductItemState>(
builder: (context, state) {
if (state.hasNoData)
return EmptyCard(
responsiveHelper: _respHelper,
);
return ListView.builder(
itemBuilder: (context, index) => index >= state.items.length
? ItemLoaderCard(
errorMsg: state.error,
retry: () => _retry(context),
responsiveHelper: _respHelper,
)
: ItemCard(
item: state.items[index],
responsiveHelper: _respHelper,
),
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);
}
}

This is the final design previewing in devices of different screen sizes. We also have a “detail” page to make changes to so that it also becomes responsive.

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

Prev —Part 3: Authentication and Login Design using the flutter_bloc package
Next —Part 5: Proper Routing in Flutter Web using beamer 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 routing and you can it out 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, #media_query #responsive_design #layout_builder

--

--