Today we are creating the UI of an awesome shopping app that is surprisingly easy to build using flutter.

Excited? Let’s get started!

Download the project source code: Github. And keep reading!

Home Screen

Start at your main.dart file make sure “MyApp” is a stateful widget.

Create two variables one called _currentPage that will allow you to know which screen is currently showing. The other is a list of the screens that we can navigate through using bottomNavigationBar.

int _currentPage = 0; final List _pages = [ HomeScreen(), CategoriesScreen(), CartScreen(), Container() ];

Once you’ve done that, let the home of you MaterialApp be a Scaffold. Set it’s background color to the the global variable bgColor that you can find on global.dart.

After that add the required BottomNavigationBarItems and make sure that the Scaffold’s body is set as follows.

home: Scaffold( backgroundColor: bgColor, bottomNavigationBar: BottomNavigationBar( selectedItemColor: Colors.black87, unselectedItemColor: Colors.grey[500], currentIndex: _currentPage, onTap: (i) { setState(() { _currentPage = i; }); }, items: [ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text("Home"), ), BottomNavigationBarItem( icon: Icon(Icons.list), title: Text("Categories"), ), BottomNavigationBarItem( icon: Icon(Icons.shopping_basket), title: Text("Cart"), ), BottomNavigationBarItem( icon: Icon(Icons.person_outline), title: Text("Profile"), ), ], ), body: SafeArea( child: _pages[_currentPage], ), ),

Now that we have finished setting up the main.dart file let’s go back to home.dart.

Everything starts with a SingleChildScrollView, this allows it’s child which in this case is a column to be scrollable.

The first child of the column represents an AppBar but in this case it’s just a Row with the first child being a Text widget and the second is an IconButton wrapped inside a circular white container.

Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Deal Finder", style: Theme.of(context) .textTheme .display1 .copyWith(fontWeight: FontWeight.bold, color: Colors.black), ), Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, ), child: IconButton( icon: Icon( Icons.search, color: Colors.black, ), onPressed: () {}, ), ), ], ),

Right after that is the list of categories. As you know in flutter when you use a ListView you have to specify its height (or set it to shrinkWrap in certain cases), so wrap a ListView inside a Container and give it a fixed height and some vertical margin.

When you have a huge list of categories it is better to use a ListView builder but since it’s not the case here I prefered using ListView.

Container( margin: const EdgeInsets.symmetric(vertical: 15), height: 45, child: ListView( scrollDirection: Axis.horizontal, children: [ Container( alignment: Alignment.center, width: MediaQuery.of(context).size.width / 3, margin: const EdgeInsets.only(right: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15.0), color: Colors.white, ), child: Text("MEN"), ), Container( alignment: Alignment.center, width: MediaQuery.of(context).size.width / 3, margin: const EdgeInsets.only(right: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15.0), color: Colors.white, ), child: Text("WOMEN"), ), Container( alignment: Alignment.center, width: MediaQuery.of(context).size.width / 3, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15.0), color: Colors.white, ), child: Text("KIDS"), ), ], ), ),

Carousel Products List Widget:

import 'package:flutter/material.dart'; import 'package:shopping_app/global.dart'; enum CarouselTypes { home, details } class CarouselProductsList extends StatefulWidget { final CarouselTypes type; final List productsList; const CarouselProductsList({ Key key, @required this.type, @required this.productsList, }) : super(key: key); @override _CarouselProductsListState createState() => _CarouselProductsListState(); } class _CarouselProductsListState extends State { int _currentIndex = 0; @override Widget build(BuildContext context) { return Container( height: 250, child: Column( children: [ Expanded( child: PageView.builder( controller: PageController( viewportFraction: widget.type == CarouselTypes.details ? .75 : .95, ), onPageChanged: (index) { setState(() { _currentIndex = index; }); }, itemCount: widget.productsList.length, itemBuilder: (ctx, id) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15.0), color: widget.type == CarouselTypes.details ? Colors.white : Colors.transparent, ), margin: widget.type == CarouselTypes.details && _currentIndex != id ? const EdgeInsets.symmetric( horizontal: 9.0, vertical: 15, ) : const EdgeInsets.symmetric( horizontal: 9, vertical: 0, ), child: ClipRRect( borderRadius: BorderRadius.circular(15.0), child: Image.network( "${widget.productsList[id]}", fit: BoxFit.cover, ), ), ); }, ), ), SizedBox(height: 9), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( widget.productsList.length, (i) { return Container( width: 9, height: 9, margin: EdgeInsets.symmetric(horizontal: 5.0), decoration: BoxDecoration( shape: BoxShape.circle, color: i == _currentIndex ? Colors.black : Colors.grey, ), ); }, ), ) ], ), ); } }

In this app there are two Carousel UIs, and I tried my best to use the same widget with a few modifications once the right type is selected.

For example if you invoke this widget and set it’s type to home, it will set the ViewPortFraction to .95 means each page takes 95% of the widget width thus allowing the other elements to be partly visible.

Inside the Container we check again if the type is set to home and if so, we just set a horizontal margin. Other than that it is pretty much the same as the carousel in the details screen.

Best Deals, Most Popular:

Both of these lists are wrapped in the same Container with a fixed height. However since you will probably have a large list of products it is preferable to use a ListViewBuilder as follows:

Container( height: 150, margin: const EdgeInsets.symmetric(vertical: 15), child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: products.length, itemBuilder: (ctx, i) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (ctx) => DetailsScreen(id: i), ), ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 9.0), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(9.0), color: Colors.white, ), child: ClipRRect( borderRadius: BorderRadius.circular(9.0), child: Image.network("${products[i].images[0]}"), ), ), ), ); }, ), ),

Flutter Shopping App UI: Categories Screen

The second UI you will be building is the Categories Screen.

Start with a Column, wrap it in a padding widget to give it more breathing space. Make sure the crossAxisAlignment of the column is set to “start”:

crossAxisAlignment: CrossAxisAlignment.start,

As you can see, the UI is split into two parts. Both are vertical lists, but the first one shows main categories and the second one shows subcategories.

To achieve this start with an expanded Row. Make sure the first child is a fixed width container that will be the parent widget for the listview.builder.

Container( width: 50, margin: const EdgeInsets.only(right: 15), child: ListView.builder( itemCount: categories.length, itemBuilder: (ctx, i) { return GestureDetector( onTap: () { setState(() { _selectedCat = i; }); }, child: Container( margin: const EdgeInsets.only(bottom: 25.0), // padding: const EdgeInsets.symmetric(vertical: 45.0), width: 50, constraints: BoxConstraints(minHeight: 101), alignment: Alignment.center, decoration: BoxDecoration( border: _selectedCat == i ? Border.all() : Border(), color: _selectedCat == i ? Colors.transparent : Colors.black, borderRadius: BorderRadius.circular(9.0), ), // child: Transform.rotate( // angle: -pi / 2, child: RotatedBox( quarterTurns: -1, child: Text( "${categories[i].title}", style: Theme.of(context) .textTheme .button .copyWith( color: _selectedCat == i ? Colors.black : Colors.white), ), ), ), ); }, ), ),

In order to achieve the rotation effect I started by using a Transform.rotate widget but it didn’t work, so I switched to RotatedBox.

You can see the condition _selectedCat == i, this allows us to know which category did the user choose and change the subcategories accordingly.

The second child UI element of the Row is responsible for showing the sub categories of the shopping app.

Expanded( flex: 4, child: ListView.builder( itemCount: categories[_selectedCat].subCat.length, itemBuilder: (ctx, i) { return Container( margin: EdgeInsets.only(bottom: 15), padding: const EdgeInsets.all(9.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( "${categories[_selectedCat].subCat[i].title}"), ), Icon(Icons.chevron_right), ], ), ); }, ), )

That’s all for the categories screen.

Flutter Shopping App UI: Cart Screen

Just like in the categories screen, we start this one with a Column inside a Padding widget.

The main element of this UI is the ListViewBuilder that returns the shopping items the user has put in his cart. Thus, it has to be expanded and take most of the screen height.

Each cart item is a simple Row, it’s first child is the product image wrapped inside a white container. (In my case the products images are in PNG so this works pretty well. If your products images don’t have a transparent background don’t set the container’s color). Second widget in the Row is an expanded Column with the product info and a Counter widget (check bellow for the code).

Now let’s take a look at the elements at the bottom of the screen.

Divider(), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("TOTAL", style: Theme.of(context).textTheme.subtitle), Text("USD. 899.01", style: Theme.of(context).textTheme.headline), ], ), ), Expanded( child: Container( height: 50, child: RaisedButton( child: Text( "CHECKOUT", style: Theme.of(context).textTheme.button.copyWith( color: Colors.white, ), ), onPressed: () {}, color: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15.0), ), ), ), ) ], ),

The Divider widget serves as a separator here.

Using a Row separate the screen in two parts, first part is to show the total price and the second is for the checkout button.

How to make a rounded button in flutter?

Very simply set it’s shape to RoundedRectangleBorder and set it’s borderRadius to BorderRadius.circular(15.0).

Counter widget:

class MyCounter extends StatefulWidget { const MyCounter({ Key key, }) : super(key: key); @override _MyCounterState createState() => _MyCounterState(); } class _MyCounterState extends State { int _currentAmount = 0; @override Widget build(BuildContext context) { return Row( children: [ GestureDetector( child: Container( padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black, ), child: Icon( Icons.remove, color: Colors.white, ), ), onTap: () { setState(() { _currentAmount -= 1; }); }, ), SizedBox(width: 15), Text( "$_currentAmount", style: Theme.of(context).textTheme.title, ), SizedBox(width: 15), GestureDetector( child: Container( padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black, ), child: Icon( Icons.add, color: Colors.white, ), ), onTap: () { setState(() { _currentAmount += 1; }); }, ), ], ); } }

I’ve already explained how this counter widget works in previous tutorials, and it’s pretty basic so let’s move on.

Flutter Shopping App UI: Product Details Page

To be continued…

I hope you enjoyed reading this tutorial. As always feel free to use the code in your own personal project. Play around with the code and improve your app development skills.

Do you like my work? Keep me posting by donating: https://www.buymeacoffee.com/bi3cp0Zk5

Don’t forget to follow me on Twitter: @cybdom and share this tutorial!