After around 2 weeks of absence I am coming back today with this awesome Cloth Store UI that we are going to code together.

Getting started:

To get started, go ahead and clone my Github repo: https://github.com/cybdom/cloth_store

Now before opening the Flutter folder I advise you to buy a coffee for yourself and another for me at https://www.buymeacoffee.com/bi3cp0Zk5, it may help both of us 😉.

Home Screen:

Let’s start by coding the home screen’s bottom app bar, as you can see it also has a floating action button docked in the middle. Here is the code to achieve that result:

bottomNavigationBar: BottomAppBar( notchMargin: 5, shape: CircularNotchedRectangle(), color: lightBlack, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: Icon( Icons.home, color: Colors.white, ), onPressed: () {}, ), IconButton( icon: Icon( Icons.search, color: Colors.white, ), onPressed: () {}, ), IconButton( icon: Icon( Icons.bookmark_border, color: Colors.white, ), onPressed: () {}, ), IconButton( icon: CircleAvatar( backgroundColor: Colors.purple, backgroundImage: NetworkImage( "https://cdn.pixabay.com/photo/2019/12/23/08/15/alaska-4714097_960_720.jpg", ), ), onPressed: () {}, ) ], ), ),

As you can see the BottomAppBar gives us the ability to set its shape and here we want it to be Notched. The child of the BottomAppBar is a Row of IconButtons which would be used for navigation.

Now that our BottomAppBar is ready, we need to set up the FloatingActionButton as follows:

floatingActionButton: FloatingActionButton( onPressed: () {}, child: Icon(Icons.apps), backgroundColor: Colors.deepPurpleAccent, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

Always under your Scaffold give it a FloatingActionButton, and a FloatingActionButtonLocation which in our case is FloatingActionButtonLocation.centerDocked.

Now that the bottom part is done, let’s go all the way up to the top with the AppBar.

appBar: AppBar( backgroundColor: lightBlack, elevation: 0.0, leading: IconButton( icon: Icon( Icons.notifications_none, color: Colors.white, ), onPressed: () {}, ), actions: [ IconButton( icon: Icon(Icons.shopping_basket), onPressed: () {}, ) ], ),

In the design the AppBar didn’t have any elevation/shadow so to prevent the default behavior we set its elevation to 0.

Just below that we have a custom widget, HomeHeader is actually a list of the clothing categories our user follows.

class HomeHeader extends StatelessWidget { const HomeHeader({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(15.0), decoration: BoxDecoration( color: lightBlack, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(35.0), bottomRight: Radius.circular(35.0), ), ), height: 101, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: categories.length, itemBuilder: (ctx, i) { return Padding( padding: i == 0 ? const EdgeInsets.only(right: 9.0) : const EdgeInsets.symmetric(horizontal: 9.0), child: Column( children: [ Container( padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( border: Border.all(color: Colors.deepPurpleAccent), shape: BoxShape.circle, ), child: CircleAvatar( backgroundColor: Colors.purple, backgroundImage: NetworkImage("${categories[i]['img']}"), ), ), Text( "${categories[i]['title']}", style: Theme.of(context).textTheme.button.copyWith( color: Colors.white, ), ) ], ), ); }, ), ), SizedBox(width: 5.0), Column( children: [ GestureDetector( onTap: () {}, child: Container( height: 50, width: 50, margin: const EdgeInsets.only(bottom: 5.0), decoration: BoxDecoration( border: Border.all(color: Colors.white), shape: BoxShape.circle, ), child: Icon(Icons.add, color: Colors.white), ), ), Text( "Add", style: Theme.of(context).textTheme.button.copyWith( color: Colors.white, ), ) ], ) ], ), ); } }

The code is pretty straight forward, just a light black container that has a border radius on the bottom two edges that takes a Row as a child.

The Row here is used to allow us to have an “ADD” category button always visible while the user is scrolling through the Expanded ListView.builder that returns each individual category.

Finally, we are getting to the main part of the home screen which is an Expanded List of the products we have for sell.

Expanded( child: ListView.builder( itemCount: products.length, itemBuilder: (ctx, i) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductScreen(id: i), ), ); }, child: Container( margin: const EdgeInsets.all(15.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: Colors.purple, backgroundImage: NetworkImage(products[i].img), ), SizedBox(width: 5.0), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( products[i].author, style: Theme.of(context) .textTheme .title .copyWith( color: Colors.white, ), ), Text( products[i].location, style: Theme.of(context) .textTheme .subtitle .copyWith(color: Colors.grey), ), ], ), ), IconButton( icon: Icon(Icons.more_vert, color: Colors.white), onPressed: () {}, ) ], ), SizedBox( height: 15.0, ), ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height / 3, minWidth: double.infinity ), child: ClipRRect( borderRadius: BorderRadius.circular(25.0), child: Image.network( products[i].img, fit: BoxFit.cover, ), ), ), SizedBox( height: 15.0, ), Text( products[i].title, style: Theme.of(context).textTheme.title.copyWith( color: Colors.white.withOpacity(.85), ), ), Row( children: [ FlatButton.icon( icon: Icon( Icons.message, color: Colors.white, ), label: Text( "${products[i].comments}", style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white), ), onPressed: () {}, ), FlatButton.icon( icon: Icon( Icons.favorite_border, color: Colors.white, ), label: Text( "${products[i].likes}", style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white), ), onPressed: () {}, ), Spacer(), IconButton( icon: Icon( Icons.bookmark_border, color: Colors.white, ), onPressed: () {}, ) ], ), ], ), ), ); }, ), )

Product Screen:

All there is in this screen is inside a Stack, for instance the top bar is a top positioned Row with two icon buttons as you can see here:

Positioned( top: 0, right: 0, left: 0, child: Row( children: [ IconButton( icon: Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), Spacer(), IconButton( icon: Icon(Icons.shopping_basket, color: Colors.white), onPressed: () {}, ), ], ), ),

Again in the same Stack we have the product image as well as the title and the author. When having text above an image sometimes the contrast isn’t ideal so here I wrapped all the text widgets inside a light black container and here is the result.

Positioned( top: 0, right: 0, left: 0, bottom: 151, child: Stack( children: [ Positioned.fill( child: Image.network( products[id].img, fit: BoxFit.cover, ), ), Positioned( bottom: 0, left: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 25.0, vertical: 45, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.transparent, Colors.black], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Column( children: [ Row( children: [ CircleAvatar( backgroundImage: NetworkImage( "https://cdn.pixabay.com/photo/2019/12/23/08/15/alaska-4714097_960_720.jpg"), backgroundColor: Colors.purple, ), SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( products[id].author, style: Theme.of(context) .textTheme .title .copyWith( color: Colors.white, ), ), Text( products[id].location, style: Theme.of(context) .textTheme .subtitle .copyWith( color: Colors.white, fontWeight: FontWeight.normal, ), ), ], ), ) ], ), SizedBox(height: 15.0), Text( products[id].title, style: Theme.of(context).textTheme.display1.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), ) ], ), ),

To finish up with this screen let’s take a look at what could’ve been a DraggableScrollableSheet (let me know if you want a tutorial about that).

In this particular app the bottom sheet is a positioned widget that has a fixed height, inside it is a black container with rounded top left and right corners.

Positioned( bottom: 0, left: 0, right: 0, height: 175, child: Container( padding: EdgeInsets.all(15.0), decoration: BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(25.0), topLeft: Radius.circular(25.0), ), color: lightBlack, ), child: Row( children: [ Expanded( child: Column( children: [ MySpecialContainer( children: [ Text( "Size", style: Theme.of(context) .textTheme .overline .copyWith(color: Colors.grey), ), Text( "Small", style: Theme.of(context) .textTheme .body2 .copyWith(color: Colors.white), ) ], ), SizedBox(height: 9.0), MySpecialContainer( children: [ Text( "Color", style: Theme.of(context) .textTheme .overline .copyWith(color: Colors.grey), ), Container( height: 5.0, decoration: BoxDecoration( color: Colors.brown, borderRadius: BorderRadius.circular(5.0), ), ) ], ) ], ), ), SizedBox(width: 15.0), Expanded( child: Column( children: [ MySpecialContainer( children: [MyCounter()], ), SizedBox(height: 9.0), MySpecialContainer( children: [ Text( "Price", style: Theme.of(context) .textTheme .overline .copyWith(color: Colors.grey), ), Text( "\$ ${products[id].price}", style: Theme.of(context) .textTheme .body2 .copyWith(color: Colors.white), ) ], ), ], ), ), SizedBox(width: 15.0), Expanded( child: Container( decoration: BoxDecoration( color: Colors.deepPurpleAccent, borderRadius: BorderRadius.circular(15.0), ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Icon( Icons.shopping_cart, color: Colors.white, size: 45, ), Text( "Add", style: Theme.of(context) .textTheme .title .copyWith(color: Colors.white), ), ], ), ), ), ], ), ), ),

My Special Container:

class MySpecialContainer extends StatelessWidget { final Function onPressed; final List children; const MySpecialContainer({ Key key, @required this.children, this.onPressed, }) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onPressed, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 9.0), decoration: BoxDecoration( border: Border.all(color: Colors.grey, width: 1), borderRadius: BorderRadius.circular(15.0), ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), ), ); } }

This widget allows vertical stacking of its children inside a rounded corners container that has some padding and an infinite width. It also handles onTap gestures which allows the user to select the size and color of the product.

My Counter:

In this blog you can find multiple examples of a counter widget, and here again is another one:

class MyCounter extends StatefulWidget { const MyCounter({ Key key, }) : super(key: key); @override _MyCounterState createState() => _MyCounterState(); } class _MyCounterState extends State { int _count = 1; @override Widget build(BuildContext context) { return Row( children: [ GestureDetector( child: Icon( Icons.add, color: Colors.white, size: 15, ), onTap: () { setState(() { _count += 1; }); }, ), Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1), ), child: Text( "$_count", style: TextStyle(color: Colors.white), ), ), ), GestureDetector( child: Icon( Icons.remove, color: Colors.white, size: 15, ), onTap: () { if (_count >= 2) { setState(() { _count--; }); } }, ), ], ); } }

As you can see there is nothing too complicated, the only checking it does is in order to avoid having less than one item selected.

Final Thoughts:

Make sure to subscribe to my Newsletter, and follow me on Twitter at @cybdom to receive a notification once that tutorial is out!

Until then, enjoy your coffee.