Faça um aplicativo Shop App em Flutter com diversos paginas
Neste tutorial, vou mostrar a você como construir um ótimo aplicativo de Shop App usando o Flutter e alguns componentes, é um projeto muito legal que vai fazer você entender alguns conceitos avançados de programação. sem mais delongas, vamos começar a construir o projeto
Configure o projeto
Primeiro você precisará criar um novo diretório chamado ‘Assets’ e adicionar as pastas do diretório ‘Assets’ do nosso github, link abaixo não se esqueça de adicionar o diretório dentro do arquivo pubspec.yaml apenas copie e cole este código dentro das dependências
dependencies: flutter: sdk: flutter flutter_svg: ^2.0.10+1 provider: ^6.1.2
Abaixo uma amostra da pasta de Assets.
Agora vamos começar a construir o aplicativo
Pastas a serem usadas, no projeto.
Código fonte:
A pasta core
contem outras duas pastas a primeira se chama components e a segunda constants, abaixo irei mostrar cada uma e seus códigos.
Lib->Src->core
->components
custom_surfix_icon.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CustomSurffixIcon extends StatelessWidget { const CustomSurffixIcon({ super.key, required this.svgIcon, }); final String svgIcon; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20), child: SvgPicture.asset( svgIcon, colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), height: 18, width: 18, ), ); } }
form_error.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class FormError extends StatelessWidget { const FormError({ super.key, required this.errors, }); final List<String?> errors; @override Widget build(BuildContext context) { return Column( children: List.generate( errors.length, (index) => formErrorText(error: errors[index]!)), ); } Row formErrorText({required String error}) { return Row( children: [ SvgPicture.asset( "assets/icons/Error.svg", height: 16, width: 16, ), const SizedBox( width: 10, ), Text(error), ], ); } }
no_account_text.dart
import 'package:flutter/material.dart'; import '../constants/constants.dart'; import '../../screens/sign_up/sign_up_screen.dart'; class NoAccountText extends StatelessWidget { const NoAccountText({ super.key, }); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( "Don’t have an account? ", style: TextStyle(fontSize: 16), ), GestureDetector( onTap: () => Navigator.pushNamed(context, SignUpScreen.routeName), child: const Text( "Sign Up", style: TextStyle(fontSize: 16, color: kPrimaryColor), ), ), ], ); } }
product_card.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../constants/constants.dart'; import '../../models/product.dart'; class ProductCard extends StatelessWidget { const ProductCard({ super.key, this.width = 140, this.aspectRetio = 1.02, required this.product, required this.onPress, }); final double width, aspectRetio; final Product product; final VoidCallback onPress; @override Widget build(BuildContext context) { return SizedBox( width: width, child: GestureDetector( onTap: onPress, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 1.02, child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: kSecondaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Image.asset(product.images[0]), ), ), const SizedBox(height: 8), Text( product.title, style: Theme.of(context).textTheme.bodyMedium, maxLines: 2, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "\$${product.price}", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: kPrimaryColor, ), ), InkWell( borderRadius: BorderRadius.circular(50), onTap: () {}, child: Container( padding: const EdgeInsets.all(6), height: 24, width: 24, decoration: BoxDecoration( color: product.isFavourite ? kPrimaryColor.withOpacity(0.15) : kSecondaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: SvgPicture.asset( "assets/icons/Heart Icon_2.svg", colorFilter: ColorFilter.mode( product.isFavourite ? const Color(0xFFFF4848) : const Color(0xFFDBDEE4), BlendMode.srcIn), ), ), ), ], ) ], ), ), ); } }
rouded_icon_btn.dart
import 'package:flutter/material.dart'; import '../constants/constants.dart'; class RoundedIconBtn extends StatelessWidget { const RoundedIconBtn({ super.key, required this.icon, required this.press, this.showShadow = false, }); final IconData icon; final GestureTapCancelCallback press; final bool showShadow; @override Widget build(BuildContext context) { return Container( height: 40, width: 40, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ if (showShadow) BoxShadow( offset: const Offset(0, 6), blurRadius: 10, color: const Color(0xFFB0B0B0).withOpacity(0.2), ), ], ), child: TextButton( style: TextButton.styleFrom( foregroundColor: kPrimaryColor, padding: EdgeInsets.zero, backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), ), onPressed: press, child: Icon(icon), ), ); } }
social_card.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class SocalCard extends StatelessWidget { const SocalCard({ super.key, this.icon, this.press, }); final String? icon; final Function? press; @override Widget build(BuildContext context) { return GestureDetector( onTap: press as void Function()?, child: Container( margin: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.all(12), height: 40, width: 40, decoration: const BoxDecoration( color: Color(0xFFF5F6F9), shape: BoxShape.circle, ), child: SvgPicture.asset(icon!), ), ); } }
->constants
constants.dart
import 'package:flutter/material.dart'; const kPrimaryColor = Color(0xFFFF7643); const kPrimaryLightColor = Color(0xFFFFECDF); const kPrimaryGradientColor = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFFA53E), Color(0xFFFF7643)], ); const kSecondaryColor = Color(0xFF979797); const kTextColor = Colors.black; const kAnimationDuration = Duration(milliseconds: 200); const headingStyle = TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black, height: 1.5, ); const defaultDuration = Duration(milliseconds: 250); // Form Error final RegExp emailValidatorRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); const String kEmailNullError = "Please Enter your email"; const String kInvalidEmailError = "Please Enter Valid Email"; const String kPassNullError = "Please Enter your password"; const String kShortPassError = "Password is too short"; const String kMatchPassError = "Passwords don't match"; const String kNamelNullError = "Please Enter your name"; const String kPhoneNumberNullError = "Please Enter your phone number"; const String kAddressNullError = "Please Enter your address"; final otpInputDecoration = InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 16), border: outlineInputBorder(), focusedBorder: outlineInputBorder(), enabledBorder: outlineInputBorder(), ); OutlineInputBorder outlineInputBorder() { return OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: kTextColor), ); }
enums.dart
enum MenuState { home, favourite, message, profile }
theme.dart
import 'package:flutter/material.dart'; import 'constants.dart'; class AppTheme { static ThemeData lightTheme(BuildContext context) { return ThemeData( scaffoldBackgroundColor: Colors.white, fontFamily: "Muli", appBarTheme: const AppBarTheme( color: Colors.white, elevation: 0, iconTheme: IconThemeData(color: Colors.black), titleTextStyle: TextStyle(color: Colors.black)), textTheme: const TextTheme( bodyLarge: TextStyle(color: kTextColor), bodyMedium: TextStyle(color: kTextColor), bodySmall: TextStyle(color: kTextColor), ), inputDecorationTheme: const InputDecorationTheme( floatingLabelBehavior: FloatingLabelBehavior.always, contentPadding: EdgeInsets.symmetric(horizontal: 42, vertical: 20), enabledBorder: outlineInputBorder, focusedBorder: outlineInputBorder, border: outlineInputBorder, ), visualDensity: VisualDensity.adaptivePlatformDensity, elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: kPrimaryColor, minimumSize: const Size(double.infinity, 48), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), ), ), ); } } const OutlineInputBorder outlineInputBorder = OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(28)), borderSide: BorderSide(color: kTextColor), gapPadding: 10, );
Lib->Src->helper
keyboard.dart
import 'package:flutter/cupertino.dart'; class KeyboardUtil { static void hideKeyboard(BuildContext context) { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } } }
Lib->Src->models
card.dart
import 'product.dart'; class Cart { final Product product; final int numOfItem; Cart({required this.product, required this.numOfItem}); } List<Cart> demoCarts = [ Cart(product: demoProducts[0], numOfItem: 2), Cart(product: demoProducts[1], numOfItem: 1), Cart(product: demoProducts[3], numOfItem: 1), ];
product.dart
import 'package:flutter/material.dart'; class Product { final int id; final String title, description; final List<String> images; final List<Color> colors; final double rating, price; final bool isFavourite, isPopular; Product({ required this.id, required this.images, required this.colors, this.rating = 0.0, this.isFavourite = false, this.isPopular = false, required this.title, required this.price, required this.description, }); } // Our demo Products List<Product> demoProducts = [ Product( id: 1, images: [ "assets/images/ps4_console_white_1.png", "assets/images/ps4_console_white_2.png", "assets/images/ps4_console_white_3.png", "assets/images/ps4_console_white_4.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Wireless Controller for PS4™", price: 64.99, description: description, rating: 4.8, isFavourite: true, isPopular: true, ), Product( id: 2, images: [ "assets/images/Image Popular Product 2.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Nike Sport White - Man Pant", price: 50.5, description: description, rating: 4.1, isPopular: true, ), Product( id: 3, images: [ "assets/images/glap.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Gloves XC Omega - Polygon", price: 36.55, description: description, rating: 4.1, isFavourite: true, isPopular: true, ), Product( id: 4, images: [ "assets/images/wireless headset.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Logitech Head", price: 20.20, description: description, rating: 4.1, isFavourite: true, ), Product( id: 1, images: [ "assets/images/ps4_console_white_1.png", "assets/images/ps4_console_white_2.png", "assets/images/ps4_console_white_3.png", "assets/images/ps4_console_white_4.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Wireless Controller for PS4™", price: 64.99, description: description, rating: 4.8, isFavourite: true, isPopular: true, ), Product( id: 2, images: [ "assets/images/Image Popular Product 2.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Nike Sport White - Man Pant", price: 50.5, description: description, rating: 4.1, isPopular: true, ), Product( id: 3, images: [ "assets/images/glap.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Gloves XC Omega - Polygon", price: 36.55, description: description, rating: 4.1, isFavourite: true, isPopular: true, ), Product( id: 4, images: [ "assets/images/wireless headset.png", ], colors: [ const Color(0xFFF6625E), const Color(0xFF836DB8), const Color(0xFFDECB9C), Colors.white, ], title: "Logitech Head", price: 20.20, description: description, rating: 4.1, isFavourite: true, ), ]; const String description = "Wireless Controller for PS4™ gives you what you want in your gaming from over precision control your games to sharing …";
Lib->Src->screens
->cart
card_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../models/card.dart'; import 'components/cart_card.dart'; import 'components/check_out_card.dart'; class CartScreen extends StatefulWidget { static String routeName = "/cart"; const CartScreen({super.key}); @override State<CartScreen> createState() => _CartScreenState(); } class _CartScreenState extends State<CartScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Column( children: [ const Text( "Your Cart", style: TextStyle(color: Colors.black), ), Text( "${demoCarts.length} items", style: Theme.of(context).textTheme.bodySmall, ), ], ), ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: ListView.builder( itemCount: demoCarts.length, itemBuilder: (context, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Dismissible( key: Key(demoCarts[index].product.id.toString()), direction: DismissDirection.endToStart, onDismissed: (direction) { setState(() { demoCarts.removeAt(index); }); }, background: Container( padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: const Color(0xFFFFE6E6), borderRadius: BorderRadius.circular(15), ), child: Row( children: [ const Spacer(), SvgPicture.asset("assets/icons/Trash.svg"), ], ), ), child: CartCard(cart: demoCarts[index]), ), ), ), ), bottomNavigationBar: const CheckoutCard(), ); } }
->cart->components
cart_card.dart
import 'package:flutter/material.dart'; import '../../../core/constants/constants.dart'; import '../../../models/card.dart'; class CartCard extends StatelessWidget { const CartCard({ super.key, required this.cart, }); final Cart cart; @override Widget build(BuildContext context) { return Row( children: [ SizedBox( width: 88, child: AspectRatio( aspectRatio: 0.88, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFFF5F6F9), borderRadius: BorderRadius.circular(15), ), child: Image.asset(cart.product.images[0]), ), ), ), const SizedBox(width: 20), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( cart.product.title, style: const TextStyle(color: Colors.black, fontSize: 16), maxLines: 2, ), const SizedBox(height: 8), Text.rich( TextSpan( text: "\$${cart.product.price}", style: const TextStyle( fontWeight: FontWeight.w600, color: kPrimaryColor), children: [ TextSpan( text: " x${cart.numOfItem}", style: Theme.of(context).textTheme.bodyLarge), ], ), ) ], ) ], ); } }
check_out_card.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../core/constants/constants.dart'; class CheckoutCard extends StatelessWidget { const CheckoutCard({ super.key, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 20, ), // height: 174, decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), boxShadow: [ BoxShadow( offset: const Offset(0, -15), blurRadius: 20, color: const Color(0xFFDADADA).withOpacity(0.15), ) ], ), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(10), height: 40, width: 40, decoration: BoxDecoration( color: const Color(0xFFF5F6F9), borderRadius: BorderRadius.circular(10), ), child: SvgPicture.asset("assets/icons/receipt.svg"), ), const Spacer(), const Text("Add voucher code"), const SizedBox(width: 8), const Icon( Icons.arrow_forward_ios, size: 12, color: kTextColor, ) ], ), const SizedBox(height: 16), Row( children: [ const Expanded( child: Text.rich( TextSpan( text: "Total:\n", children: [ TextSpan( text: "\$337.15", style: TextStyle(fontSize: 16, color: Colors.black), ), ], ), ), ), Expanded( child: ElevatedButton( onPressed: () {}, child: const Text("Check Out"), ), ), ], ), ], ), ), ); } }
->complete_profile
complete_profile_screen.dart
import 'package:flutter/material.dart'; import '../../core/constants/constants.dart'; import 'components/complete_profile_form.dart'; class CompleteProfileScreen extends StatelessWidget { static String routeName = "/complete_profile"; const CompleteProfileScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Sign Up'), ), body: SafeArea( child: SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 16), const Text("Complete Profile", style: headingStyle), const Text( "Complete your details or continue \nwith social media", textAlign: TextAlign.center, ), const SizedBox(height: 16), const CompleteProfileForm(), const SizedBox(height: 30), Text( "By continuing your confirm that you agree \nwith our Term and Condition", textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ), ), ); } }
->complete_profile->components
complete_profile_form.dart
import 'package:flutter/material.dart'; import '../../../core/components/custom_surfix_icon.dart'; import '../../../core/components/form_error.dart'; import '../../../core/constants/constants.dart'; import '../../otp/otp_screen.dart'; class CompleteProfileForm extends StatefulWidget { const CompleteProfileForm({super.key}); @override CompleteProfileFormState createState() => CompleteProfileFormState(); } class CompleteProfileFormState extends State<CompleteProfileForm> { final _formKey = GlobalKey<FormState>(); final List<String?> errors = []; String? firstName; String? lastName; String? phoneNumber; String? address; void addError({String? error}) { if (!errors.contains(error)) { setState(() { errors.add(error); }); } } void removeError({String? error}) { if (errors.contains(error)) { setState(() { errors.remove(error); }); } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( onSaved: (newValue) => firstName = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kNamelNullError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kNamelNullError); return ""; } return null; }, decoration: const InputDecoration( labelText: "First Name", hintText: "Enter your first name", // If you are using latest version of flutter then lable text and hint text shown like this // if you r using flutter less then 1.20.* then maybe this is not working properly floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/User.svg"), ), ), const SizedBox(height: 20), TextFormField( onSaved: (newValue) => lastName = newValue, decoration: const InputDecoration( labelText: "Last Name", hintText: "Enter your last name", // If you are using latest version of flutter then lable text and hint text shown like this // if you r using flutter less then 1.20.* then maybe this is not working properly floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/User.svg"), ), ), const SizedBox(height: 20), TextFormField( keyboardType: TextInputType.phone, onSaved: (newValue) => phoneNumber = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kPhoneNumberNullError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kPhoneNumberNullError); return ""; } return null; }, decoration: const InputDecoration( labelText: "Phone Number", hintText: "Enter your phone number", // If you are using latest version of flutter then lable text and hint text shown like this // if you r using flutter less then 1.20.* then maybe this is not working properly floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/Phone.svg"), ), ), const SizedBox(height: 20), TextFormField( onSaved: (newValue) => address = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kAddressNullError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kAddressNullError); return ""; } return null; }, decoration: const InputDecoration( labelText: "Address", hintText: "Enter your address", // If you are using latest version of flutter then lable text and hint text shown like this // if you r using flutter less then 1.20.* then maybe this is not working properly floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/Location point.svg"), ), ), FormError(errors: errors), const SizedBox(height: 20), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { Navigator.pushNamed(context, OtpScreen.routeName); } }, child: const Text("Continue"), ), ], ), ); } }
->details
detail_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:shop_app/src/screens/cart/cart_screen.dart'; import '../../models/product.dart'; import 'components/color_dots.dart'; import 'components/product_description.dart'; import 'components/product_images.dart'; import 'components/top_rounded_container.dart'; class DetailsScreen extends StatelessWidget { static String routeName = "/details"; const DetailsScreen({super.key}); @override Widget build(BuildContext context) { final ProductDetailsArguments agrs = ModalRoute.of(context)!.settings.arguments as ProductDetailsArguments; final product = agrs.product; return Scaffold( extendBody: true, extendBodyBehindAppBar: true, backgroundColor: const Color(0xFFF5F6F9), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () { Navigator.pop(context); }, style: ElevatedButton.styleFrom( shape: const CircleBorder(), padding: EdgeInsets.zero, elevation: 0, backgroundColor: Colors.white, ), child: const Icon( Icons.arrow_back_ios_new, color: Colors.black, size: 20, ), ), ), actions: [ Row( children: [ Container( margin: const EdgeInsets.only(right: 20), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: Row( children: [ const Text( "4.7", style: TextStyle( fontSize: 14, color: Colors.black, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), SvgPicture.asset("assets/icons/Star Icon.svg"), ], ), ), ], ), ], ), body: ListView( children: [ ProductImages(product: product), TopRoundedContainer( color: Colors.white, child: Column( children: [ ProductDescription( product: product, pressOnSeeMore: () {}, ), TopRoundedContainer( color: const Color(0xFFF6F7F9), child: Column( children: [ ColorDots(product: product), ], ), ), ], ), ), ], ), bottomNavigationBar: TopRoundedContainer( color: Colors.white, child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: ElevatedButton( onPressed: () { Navigator.pushNamed(context, CartScreen.routeName); }, child: const Text("Add To Cart"), ), ), ), ), ); } } class ProductDetailsArguments { final Product product; ProductDetailsArguments({required this.product}); }
->details->components
color_dots.dart
import 'package:flutter/material.dart'; import '../../../core/components/rounded_icon_btn.dart'; import '../../../core/constants/constants.dart'; import '../../../models/product.dart'; class ColorDots extends StatelessWidget { const ColorDots({ super.key, required this.product, }); final Product product; @override Widget build(BuildContext context) { // Now this is fixed and only for demo int selectedColor = 3; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ ...List.generate( product.colors.length, (index) => ColorDot( color: product.colors[index], isSelected: index == selectedColor, ), ), const Spacer(), RoundedIconBtn( icon: Icons.remove, press: () {}, ), const SizedBox(width: 20), RoundedIconBtn( icon: Icons.add, showShadow: true, press: () {}, ), ], ), ); } } class ColorDot extends StatelessWidget { const ColorDot({ super.key, required this.color, this.isSelected = false, }); final Color color; final bool isSelected; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(right: 2), padding: const EdgeInsets.all(8), height: 40, width: 40, decoration: BoxDecoration( color: Colors.transparent, border: Border.all(color: isSelected ? kPrimaryColor : Colors.transparent), shape: BoxShape.circle, ), child: DecoratedBox( decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), ); } }
product_description.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../core/constants/constants.dart'; import '../../../models/product.dart'; class ProductDescription extends StatelessWidget { const ProductDescription({ super.key, required this.product, this.pressOnSeeMore, }); final Product product; final GestureTapCallback? pressOnSeeMore; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( product.title, style: Theme.of(context).textTheme.titleLarge, ), ), Align( alignment: Alignment.centerRight, child: Container( padding: const EdgeInsets.all(16), width: 48, decoration: BoxDecoration( color: product.isFavourite ? const Color(0xFFFFE6E6) : const Color(0xFFF5F6F9), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), bottomLeft: Radius.circular(20), ), ), child: SvgPicture.asset( "assets/icons/Heart Icon_2.svg", colorFilter: ColorFilter.mode( product.isFavourite ? const Color(0xFFFF4848) : const Color(0xFFDBDEE4), BlendMode.srcIn), height: 16, ), ), ), Padding( padding: const EdgeInsets.only( left: 20, right: 64, ), child: Text( product.description, maxLines: 3, ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), child: GestureDetector( onTap: () {}, child: const Row( children: [ Text( "See More Detail", style: TextStyle( fontWeight: FontWeight.w600, color: kPrimaryColor), ), SizedBox(width: 5), Icon( Icons.arrow_forward_ios, size: 12, color: kPrimaryColor, ), ], ), ), ) ], ); } }
product_images.dart
import 'package:flutter/material.dart'; import '../../../core/constants/constants.dart'; import '../../../models/product.dart'; class ProductImages extends StatefulWidget { const ProductImages({ super.key, required this.product, }); final Product product; @override ProductImagesState createState() => ProductImagesState(); } class ProductImagesState extends State<ProductImages> { int selectedImage = 0; @override Widget build(BuildContext context) { return Column( children: [ SizedBox( width: 238, child: AspectRatio( aspectRatio: 1, child: Image.asset(widget.product.images[selectedImage]), ), ), // SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ...List.generate( widget.product.images.length, (index) => SmallProductImage( isSelected: index == selectedImage, press: () { setState(() { selectedImage = index; }); }, image: widget.product.images[index], ), ), ], ) ], ); } } class SmallProductImage extends StatefulWidget { const SmallProductImage( {super.key, required this.isSelected, required this.press, required this.image}); final bool isSelected; final VoidCallback press; final String image; @override State<SmallProductImage> createState() => _SmallProductImageState(); } class _SmallProductImageState extends State<SmallProductImage> { @override Widget build(BuildContext context) { return GestureDetector( onTap: widget.press, child: AnimatedContainer( duration: defaultDuration, margin: const EdgeInsets.only(right: 16), padding: const EdgeInsets.all(8), height: 48, width: 48, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: kPrimaryColor.withOpacity(widget.isSelected ? 1 : 0)), ), child: Image.asset(widget.image), ), ); } }
top_rounded_conteiner.dart
import 'package:flutter/material.dart'; class TopRoundedContainer extends StatelessWidget { const TopRoundedContainer({ super.key, required this.color, required this.child, }); final Color color; final Widget child; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20), width: double.infinity, decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.only( topLeft: Radius.circular(40), topRight: Radius.circular(40), ), ), child: child, ); } }
->favorite
favorite_screen.dart
import 'package:flutter/material.dart'; import 'package:shop_app/src/core/components/product_card.dart'; import 'package:shop_app/src/models/product.dart'; import '../details/details_screen.dart'; class FavoriteScreen extends StatelessWidget { const FavoriteScreen({super.key}); @override Widget build(BuildContext context) { return SafeArea( child: Column( children: [ Text( "Favorites", style: Theme.of(context).textTheme.titleLarge, ), Expanded( child: Padding( padding: const EdgeInsets.all(16), child: GridView.builder( itemCount: demoProducts.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, childAspectRatio: 0.7, mainAxisSpacing: 20, crossAxisSpacing: 16, ), itemBuilder: (context, index) => ProductCard( product: demoProducts[index], onPress: () => Navigator.pushNamed( context, DetailsScreen.routeName, arguments: ProductDetailsArguments(product: demoProducts[index]), ), ), ), ), ) ], ), ); } }
->forgot_password
forgot_password_screen.dart
import 'package:flutter/material.dart'; import 'components/forgot_pass_form.dart'; class ForgotPasswordScreen extends StatelessWidget { static String routeName = "/forgot_password"; const ForgotPasswordScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Forgot Password"), ), body: const SizedBox( width: double.infinity, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ SizedBox(height: 16), Text( "Forgot Password", style: TextStyle( fontSize: 24, color: Colors.black, fontWeight: FontWeight.bold, ), ), Text( "Please enter your email and we will send \nyou a link to return to your account", textAlign: TextAlign.center, ), SizedBox(height: 32), ForgotPassForm(), ], ), ), ), ), ); } }
->forgot_password->components
forgot_pass_form.dart
import 'package:flutter/material.dart'; import '../../../core/components/custom_surfix_icon.dart'; import '../../../core/components/form_error.dart'; import '../../../core/components/no_account_text.dart'; import '../../../core/constants/constants.dart'; class ForgotPassForm extends StatefulWidget { const ForgotPassForm({super.key}); @override ForgotPassFormState createState() => ForgotPassFormState(); } class ForgotPassFormState extends State<ForgotPassForm> { final _formKey = GlobalKey<FormState>(); List<String> errors = []; String? email; @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( keyboardType: TextInputType.emailAddress, onSaved: (newValue) => email = newValue, onChanged: (value) { if (value.isNotEmpty && errors.contains(kEmailNullError)) { setState(() { errors.remove(kEmailNullError); }); } else if (emailValidatorRegExp.hasMatch(value) && errors.contains(kInvalidEmailError)) { setState(() { errors.remove(kInvalidEmailError); }); } return; }, validator: (value) { if (value!.isEmpty && !errors.contains(kEmailNullError)) { setState(() { errors.add(kEmailNullError); }); } else if (!emailValidatorRegExp.hasMatch(value) && !errors.contains(kInvalidEmailError)) { setState(() { errors.add(kInvalidEmailError); }); } return null; }, decoration: const InputDecoration( labelText: "Email", hintText: "Enter your email", // If you are using latest version of flutter then lable text and hint text shown like this // if you r using flutter less then 1.20.* then maybe this is not working properly floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/Mail.svg"), ), ), const SizedBox(height: 8), FormError(errors: errors), const SizedBox(height: 8), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Do what you want to do } }, child: const Text("Continue"), ), const SizedBox(height: 16), const NoAccountText(), ], ), ); } }
->home
home_screen.dart
import 'package:flutter/material.dart'; import 'components/categories.dart'; import 'components/discount_banner.dart'; import 'components/home_header.dart'; import 'components/popular_product.dart'; import 'components/special_offers.dart'; class HomeScreen extends StatelessWidget { static String routeName = "/home"; const HomeScreen({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: SafeArea( child: SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 16), child: Column( children: [ HomeHeader(), DiscountBanner(), Categories(), SpecialOffers(), SizedBox(height: 20), PopularProducts(), SizedBox(height: 20), ], ), ), ), ); } }
->home->components
categories.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class Categories extends StatelessWidget { const Categories({super.key}); @override Widget build(BuildContext context) { List<Map<String, dynamic>> categories = [ {"icon": "assets/icons/Flash Icon.svg", "text": "Flash Deal"}, {"icon": "assets/icons/Bill Icon.svg", "text": "Bill"}, {"icon": "assets/icons/Game Icon.svg", "text": "Game"}, {"icon": "assets/icons/Gift Icon.svg", "text": "Daily Gift"}, {"icon": "assets/icons/Discover.svg", "text": "More"}, ]; return Padding( padding: const EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: List.generate( categories.length, (index) => CategoryCard( icon: categories[index]["icon"], text: categories[index]["text"], press: () {}, ), ), ), ); } } class CategoryCard extends StatelessWidget { const CategoryCard({ super.key, required this.icon, required this.text, required this.press, }); final String icon, text; final GestureTapCallback press; @override Widget build(BuildContext context) { return GestureDetector( onTap: press, child: Column( children: [ Container( padding: const EdgeInsets.all(16), height: 56, width: 56, decoration: BoxDecoration( color: const Color(0xFFFFECDF), borderRadius: BorderRadius.circular(10), ), child: SvgPicture.asset(icon), ), const SizedBox(height: 4), Text(text, textAlign: TextAlign.center) ], ), ); } }
discount_banner.dart
import 'package:flutter/material.dart'; class DiscountBanner extends StatelessWidget { const DiscountBanner({ super.key, }); @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.all(20), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), decoration: BoxDecoration( color: const Color(0xFF4A3298), borderRadius: BorderRadius.circular(20), ), child: const Text.rich( TextSpan( style: TextStyle(color: Colors.white), children: [ TextSpan(text: "A Summer Surpise\n"), TextSpan( text: "Cashback 20%", style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ], ), ), ); } }
home_header.dart
import 'package:flutter/material.dart'; import '../../cart/cart_screen.dart'; import 'icon_btn_with_counter.dart'; import 'search_field.dart'; class HomeHeader extends StatelessWidget { const HomeHeader({ super.key, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded(child: SearchField()), const SizedBox(width: 16), IconBtnWithCounter( svgSrc: "assets/icons/Cart Icon.svg", press: () => Navigator.pushNamed(context, CartScreen.routeName), ), const SizedBox(width: 8), IconBtnWithCounter( svgSrc: "assets/icons/Bell.svg", numOfitem: 3, press: () {}, ), ], ), ); } }
icon_btn_with_counter.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../core/constants/constants.dart'; class IconBtnWithCounter extends StatelessWidget { const IconBtnWithCounter({ super.key, required this.svgSrc, this.numOfitem = 0, required this.press, }); final String svgSrc; final int numOfitem; final GestureTapCallback press; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(100), onTap: press, child: Stack( clipBehavior: Clip.none, children: [ Container( padding: const EdgeInsets.all(12), height: 46, width: 46, decoration: BoxDecoration( color: kSecondaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: SvgPicture.asset(svgSrc), ), if (numOfitem != 0) Positioned( top: -3, right: 0, child: Container( height: 20, width: 20, decoration: BoxDecoration( color: const Color(0xFFFF4848), shape: BoxShape.circle, border: Border.all(width: 1.5, color: Colors.white), ), child: Center( child: Text( "$numOfitem", style: const TextStyle( fontSize: 12, height: 1, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ) ], ), ); } }
popular_product.dart
import 'package:flutter/material.dart'; import '../../../core/components/product_card.dart'; import '../../../models/product.dart'; import '../../details/details_screen.dart'; import '../../products/products_screen.dart'; import 'section_title.dart'; class PopularProducts extends StatelessWidget { const PopularProducts({super.key}); @override Widget build(BuildContext context) { return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SectionTitle( title: "Popular Products", press: () { Navigator.pushNamed(context, ProductsScreen.routeName); }, ), ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ ...List.generate( demoProducts.length, (index) { if (demoProducts[index].isPopular) { return Padding( padding: const EdgeInsets.only(left: 20), child: ProductCard( product: demoProducts[index], onPress: () => Navigator.pushNamed( context, DetailsScreen.routeName, arguments: ProductDetailsArguments( product: demoProducts[index]), ), ), ); } return const SizedBox .shrink(); // here by default width and height is 0 }, ), const SizedBox(width: 20), ], ), ) ], ); } }
search_field.dart
import 'package:flutter/material.dart'; import '../../../core/constants/constants.dart'; class SearchField extends StatelessWidget { const SearchField({ super.key, }); @override Widget build(BuildContext context) { return Form( child: TextFormField( onChanged: (value) {}, decoration: InputDecoration( filled: true, fillColor: kSecondaryColor.withOpacity(0.1), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), border: searchOutlineInputBorder, focusedBorder: searchOutlineInputBorder, enabledBorder: searchOutlineInputBorder, hintText: "Search product", prefixIcon: const Icon(Icons.search), ), ), ); } } const searchOutlineInputBorder = OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide.none, );
section_title.dart
import 'package:flutter/material.dart'; class SectionTitle extends StatelessWidget { const SectionTitle({ super.key, required this.title, required this.press, }); final String title; final GestureTapCallback press; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black, ), ), TextButton( onPressed: press, style: TextButton.styleFrom(foregroundColor: Colors.grey), child: const Text("See more"), ), ], ); } }
special_offers.dart
import 'package:flutter/material.dart'; import 'package:shop_app/src/screens/products/products_screen.dart'; import 'section_title.dart'; class SpecialOffers extends StatelessWidget { const SpecialOffers({ super.key, }); @override Widget build(BuildContext context) { return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SectionTitle( title: "Special for you", press: () {}, ), ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ SpecialOfferCard( image: "assets/images/Image Banner 2.png", category: "Smartphone", numOfBrands: 18, press: () { Navigator.pushNamed(context, ProductsScreen.routeName); }, ), SpecialOfferCard( image: "assets/images/Image Banner 3.png", category: "Fashion", numOfBrands: 24, press: () { Navigator.pushNamed(context, ProductsScreen.routeName); }, ), const SizedBox(width: 20), ], ), ), ], ); } } class SpecialOfferCard extends StatelessWidget { const SpecialOfferCard({ super.key, required this.category, required this.image, required this.numOfBrands, required this.press, }); final String category, image; final int numOfBrands; final GestureTapCallback press; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 20), child: GestureDetector( onTap: press, child: SizedBox( width: 242, height: 100, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Stack( children: [ Image.asset( image, fit: BoxFit.cover, ), Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black54, Colors.black38, Colors.black26, Colors.transparent, ], ), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 10, ), child: Text.rich( TextSpan( style: const TextStyle(color: Colors.white), children: [ TextSpan( text: "$category\n", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), TextSpan(text: "$numOfBrands Brands") ], ), ), ), ], ), ), ), ), ); } }
->login_success
login_success_screen.dart
import 'package:flutter/material.dart'; import 'package:shop_app/src/screens/init_screen.dart'; class LoginSuccessScreen extends StatelessWidget { static String routeName = "/login_success"; const LoginSuccessScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: const SizedBox(), title: const Text("Login Success"), ), body: Column( children: [ const SizedBox(height: 16), Image.asset( "assets/images/success.png", height: MediaQuery.of(context).size.height * 0.4, //40% ), const SizedBox(height: 16), const Text( "Login Success", style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: Colors.black, ), ), const Spacer(), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: ElevatedButton( onPressed: () { Navigator.pushNamed(context, InitScreen.routeName); }, child: const Text("Back to home"), ), ), const Spacer(), ], ), ); } }
->otp
otp_screen.dart
import 'package:flutter/material.dart'; import '../../core/constants/constants.dart'; import 'components/otp_form.dart'; class OtpScreen extends StatelessWidget { static String routeName = "/otp"; const OtpScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("OTP Verification"), ), body: SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 16), const Text( "OTP Verification", style: headingStyle, ), const Text("We sent your code to +1 898 860 ***"), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("This code will expired in "), TweenAnimationBuilder( tween: Tween(begin: 30.0, end: 0.0), duration: const Duration(seconds: 30), builder: (_, dynamic value, child) => Text( "00:${value.toInt()}", style: const TextStyle(color: kPrimaryColor), ), ), ], ), const OtpForm(), const SizedBox(height: 20), GestureDetector( onTap: () { // OTP code resend }, child: const Text( "Resend OTP Code", style: TextStyle(decoration: TextDecoration.underline), ), ) ], ), ), ), ), ); } }
->otp->components
otp_form.dart
import 'package:flutter/material.dart'; import '../../../core/constants/constants.dart'; import '../../login_success/login_success_screen.dart'; class OtpForm extends StatefulWidget { const OtpForm({ super.key, }); @override OtpFormState createState() => OtpFormState(); } class OtpFormState extends State<OtpForm> { FocusNode? pin2FocusNode; FocusNode? pin3FocusNode; FocusNode? pin4FocusNode; @override void initState() { super.initState(); pin2FocusNode = FocusNode(); pin3FocusNode = FocusNode(); pin4FocusNode = FocusNode(); } @override void dispose() { super.dispose(); pin2FocusNode!.dispose(); pin3FocusNode!.dispose(); pin4FocusNode!.dispose(); } void nextField(String value, FocusNode? focusNode) { if (value.length == 1) { focusNode!.requestFocus(); } } @override Widget build(BuildContext context) { return Form( child: Column( children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.15), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 60, child: TextFormField( autofocus: true, obscureText: true, style: const TextStyle(fontSize: 24), keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: otpInputDecoration, onChanged: (value) { nextField(value, pin2FocusNode); }, ), ), SizedBox( width: 60, child: TextFormField( focusNode: pin2FocusNode, obscureText: true, style: const TextStyle(fontSize: 24), keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: otpInputDecoration, onChanged: (value) => nextField(value, pin3FocusNode), ), ), SizedBox( width: 60, child: TextFormField( focusNode: pin3FocusNode, obscureText: true, style: const TextStyle(fontSize: 24), keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: otpInputDecoration, onChanged: (value) => nextField(value, pin4FocusNode), ), ), SizedBox( width: 60, child: TextFormField( focusNode: pin4FocusNode, obscureText: true, style: const TextStyle(fontSize: 24), keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: otpInputDecoration, onChanged: (value) { if (value.length == 1) { pin4FocusNode!.unfocus(); // Then you need to check is the code is correct or not } }, ), ), ], ), SizedBox(height: MediaQuery.of(context).size.height * 0.15), ElevatedButton( onPressed: () { Navigator.pushNamed(context, LoginSuccessScreen.routeName); }, child: const Text("Continue"), ), ], ), ); } }
->products
products_screen.dart
import 'package:flutter/material.dart'; import 'package:shop_app/src/core/components/product_card.dart'; import 'package:shop_app/src/models/product.dart'; import '../details/details_screen.dart'; class ProductsScreen extends StatelessWidget { const ProductsScreen({super.key}); static String routeName = "/products"; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Products"), ), body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: GridView.builder( itemCount: demoProducts.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, childAspectRatio: 0.7, mainAxisSpacing: 20, crossAxisSpacing: 16, ), itemBuilder: (context, index) => ProductCard( product: demoProducts[index], onPress: () => Navigator.pushNamed( context, DetailsScreen.routeName, arguments: ProductDetailsArguments(product: demoProducts[index]), ), ), ), ), ), ); } }
->profile
profile_screen.dart
import 'package:flutter/material.dart'; import 'components/profile_menu.dart'; import 'components/profile_pic.dart'; class ProfileScreen extends StatelessWidget { static String routeName = "/profile"; const ProfileScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Profile"), ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( children: [ const ProfilePic(), const SizedBox(height: 20), ProfileMenu( text: "My Account", icon: "assets/icons/User Icon.svg", press: () => {}, ), ProfileMenu( text: "Notifications", icon: "assets/icons/Bell.svg", press: () {}, ), ProfileMenu( text: "Settings", icon: "assets/icons/Settings.svg", press: () {}, ), ProfileMenu( text: "Help Center", icon: "assets/icons/Question mark.svg", press: () {}, ), ProfileMenu( text: "Log Out", icon: "assets/icons/Log out.svg", press: () {}, ), ], ), ), ); } }
->profile->components
profile_menu.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../core/constants/constants.dart'; class ProfileMenu extends StatelessWidget { const ProfileMenu({ super.key, required this.text, required this.icon, this.press, }); final String text, icon; final VoidCallback? press; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: TextButton( style: TextButton.styleFrom( foregroundColor: kPrimaryColor, padding: const EdgeInsets.all(20), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), backgroundColor: const Color(0xFFF5F6F9), ), onPressed: press, child: Row( children: [ SvgPicture.asset( icon, colorFilter: const ColorFilter.mode( kPrimaryColor, BlendMode.srcIn, ), width: 22, ), const SizedBox(width: 20), Expanded(child: Text(text)), const Icon(Icons.arrow_forward_ios), ], ), ), ); } }
profile_pic.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class ProfilePic extends StatelessWidget { const ProfilePic({ super.key, }); @override Widget build(BuildContext context) { return SizedBox( height: 115, width: 115, child: Stack( fit: StackFit.expand, clipBehavior: Clip.none, children: [ const CircleAvatar( backgroundImage: AssetImage("assets/images/Profile Image.png"), ), Positioned( right: -16, bottom: 0, child: SizedBox( height: 46, width: 46, child: TextButton( style: TextButton.styleFrom( foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), side: const BorderSide(color: Colors.white), ), backgroundColor: const Color(0xFFF5F6F9), ), onPressed: () {}, child: SvgPicture.asset("assets/icons/Camera Icon.svg"), ), ), ) ], ), ); } }
->sign_in
sign_in_screen.dart
import 'package:flutter/material.dart'; import '../../core/components/no_account_text.dart'; import '../../core/components/socal_card.dart'; import 'components/sign_form.dart'; class SignInScreen extends StatelessWidget { static String routeName = "/sign_in"; const SignInScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Sign In"), ), body: SafeArea( child: SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 16), const Text( "Welcome Back", style: TextStyle( color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold, ), ), const Text( "Sign in with your email and password \nor continue with social media", textAlign: TextAlign.center, ), const SizedBox(height: 16), const SignForm(), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SocalCard( icon: "assets/icons/google-icon.svg", press: () {}, ), SocalCard( icon: "assets/icons/facebook-2.svg", press: () {}, ), SocalCard( icon: "assets/icons/twitter.svg", press: () {}, ), ], ), const SizedBox(height: 20), const NoAccountText(), ], ), ), ), ), ), ); } }
->sign_in->components
sign_form.dart
import 'package:flutter/material.dart'; import '../../../core/components/custom_surfix_icon.dart'; import '../../../core/components/form_error.dart'; import '../../../core/constants/constants.dart'; import '../../../helper/keyboard.dart'; import '../../forgot_password/forgot_password_screen.dart'; import '../../login_success/login_success_screen.dart'; class SignForm extends StatefulWidget { const SignForm({super.key}); @override SignFormState createState() => SignFormState(); } class SignFormState extends State<SignForm> { final _formKey = GlobalKey<FormState>(); final _searchQueryEC = TextEditingController(); var _obscurePassword = true; String? email; String? password; bool? remember = false; final List<String?> errors = []; void addError({String? error}) { if (!errors.contains(error)) { setState(() { errors.add(error); }); } } void removeError({String? error}) { if (errors.contains(error)) { setState(() { errors.remove(error); }); } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( keyboardType: TextInputType.emailAddress, onSaved: (newValue) => email = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kEmailNullError); } else if (emailValidatorRegExp.hasMatch(value)) { removeError(error: kInvalidEmailError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kEmailNullError); return ""; } else if (!emailValidatorRegExp.hasMatch(value)) { addError(error: kInvalidEmailError); return ""; } return null; }, decoration: const InputDecoration( labelText: "Email", hintText: "Enter your email", floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/Mail.svg"), ), ), const SizedBox(height: 20), TextFormField( controller: _searchQueryEC, obscureText: _obscurePassword ? true : false, onSaved: (newValue) => password = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kPassNullError); } else if (value.length >= 8) { removeError(error: kShortPassError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kPassNullError); return ""; } else if (value.length < 8) { addError(error: kShortPassError); return ""; } return null; }, decoration: InputDecoration( labelText: "Password", hintText: "Enter your password", floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: ListenableBuilder( listenable: _searchQueryEC, builder: (_, __) => _searchQueryEC.text.isNotEmpty ? IconButton( onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, icon: _obscurePassword ? const CustomSurffixIcon( svgIcon: "assets/icons/lock-close.svg") : const CustomSurffixIcon( svgIcon: "assets/icons/lock-open.svg"), ) : const CustomSurffixIcon(svgIcon: "assets/icons/lock-close.svg"), ), ), ), const SizedBox(height: 20), Row( children: [ Checkbox( value: remember, activeColor: kPrimaryColor, onChanged: (value) { setState(() { remember = value; }); }, ), const Text("Remember me"), const Spacer(), GestureDetector( onTap: () => Navigator.pushNamed( context, ForgotPasswordScreen.routeName), child: const Text( "Forgot Password", style: TextStyle(decoration: TextDecoration.underline), ), ) ], ), FormError(errors: errors), const SizedBox(height: 16), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); // if all are valid then go to success screen KeyboardUtil.hideKeyboard(context); Navigator.pushNamed(context, LoginSuccessScreen.routeName); } }, child: const Text("Continue"), ), ], ), ); } }
->sign_up
sign_up_screen.dart
import 'package:flutter/material.dart'; import '../../core/components/socal_card.dart'; import '../../core/constants/constants.dart'; import 'components/sign_up_form.dart'; class SignUpScreen extends StatelessWidget { static String routeName = "/sign_up"; const SignUpScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Sign Up"), ), body: SafeArea( child: SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 16), const Text("Register Account", style: headingStyle), const Text( "Complete your details or continue \nwith social media", textAlign: TextAlign.center, ), const SizedBox(height: 16), const SignUpForm(), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SocalCard( icon: "assets/icons/google-icon.svg", press: () {}, ), SocalCard( icon: "assets/icons/facebook-2.svg", press: () {}, ), SocalCard( icon: "assets/icons/twitter.svg", press: () {}, ), ], ), const SizedBox(height: 16), Text( 'By continuing your confirm that you agree \nwith our Term and Condition', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ) ], ), ), ), ), ), ); } }
->sign_up->components
sign_up_form.dart
import 'package:flutter/material.dart'; import '../../../core/components/custom_surfix_icon.dart'; import '../../../core/components/form_error.dart'; import '../../../core/constants/constants.dart'; import '../../complete_profile/complete_profile_screen.dart'; class SignUpForm extends StatefulWidget { const SignUpForm({super.key}); @override SignUpFormState createState() => SignUpFormState(); } class SignUpFormState extends State<SignUpForm> { final _formKey = GlobalKey<FormState>(); String? email; String? password; String? conformPassword; bool remember = false; var _obscurePassword = true; final _searchQueryEC = TextEditingController(); var _obscurePasswordConfirm = true; final _searchQueryECConfrm = TextEditingController(); final List<String?> errors = []; void addError({String? error}) { if (!errors.contains(error)) { setState(() { errors.add(error); }); } } void removeError({String? error}) { if (errors.contains(error)) { setState(() { errors.remove(error); }); } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( keyboardType: TextInputType.emailAddress, onSaved: (newValue) => email = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kEmailNullError); } else if (emailValidatorRegExp.hasMatch(value)) { removeError(error: kInvalidEmailError); } return; }, validator: (value) { if (value!.isEmpty) { addError(error: kEmailNullError); return ""; } else if (!emailValidatorRegExp.hasMatch(value)) { addError(error: kInvalidEmailError); return ""; } return null; }, decoration: const InputDecoration( labelText: "Email", hintText: "Enter your email", floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: CustomSurffixIcon(svgIcon: "assets/icons/Mail.svg"), ), ), const SizedBox(height: 20), TextFormField( controller: _searchQueryEC, obscureText: _obscurePassword ? true : false, onSaved: (newValue) => password = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kPassNullError); } else if (value.length >= 8) { removeError(error: kShortPassError); } password = value; }, validator: (value) { if (value!.isEmpty) { addError(error: kPassNullError); return ""; } else if (value.length < 8) { addError(error: kShortPassError); return ""; } return null; }, decoration: InputDecoration( labelText: "Password", hintText: "Enter your password", floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: ListenableBuilder( listenable: _searchQueryEC, builder: (_, __) => _searchQueryEC.text.isNotEmpty ? IconButton( onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, icon: _obscurePassword ? const CustomSurffixIcon( svgIcon: "assets/icons/lock-close.svg") : const CustomSurffixIcon( svgIcon: "assets/icons/lock-open.svg"), ) : const CustomSurffixIcon( svgIcon: "assets/icons/lock-close.svg"), ), ), ), const SizedBox(height: 20), TextFormField( controller: _searchQueryECConfrm, obscureText: _obscurePasswordConfirm ? true : false, onSaved: (newValue) => conformPassword = newValue, onChanged: (value) { if (value.isNotEmpty) { removeError(error: kPassNullError); } else if (value.isNotEmpty && password == conformPassword) { removeError(error: kMatchPassError); } conformPassword = value; }, validator: (value) { if (value!.isEmpty) { addError(error: kPassNullError); return ""; } else if ((password != value)) { addError(error: kMatchPassError); return ""; } return null; }, decoration: InputDecoration( labelText: "Confirm Password", hintText: "Re-enter your password", floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: ListenableBuilder( listenable: _searchQueryECConfrm, builder: (_, __) => _searchQueryECConfrm.text.isNotEmpty ? IconButton( onPressed: () { setState(() { _obscurePasswordConfirm = !_obscurePasswordConfirm; }); }, icon: Icon( _obscurePasswordConfirm ? Icons.visibility : Icons.visibility_off, size: 20, color: Colors.blueGrey, ), ) : const CustomSurffixIcon( svgIcon: "assets/icons/lock-close.svg"), ), ), ), FormError(errors: errors), const SizedBox(height: 20), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); Navigator.pushNamed(context, CompleteProfileScreen.routeName); } }, child: const Text("Continue"), ), ], ), ); } }
->splash
splesh_screen.dart
import 'package:flutter/material.dart'; import '../../core/constants/constants.dart'; import '../sign_in/sign_in_screen.dart'; import 'components/splash_content.dart'; class SplashScreen extends StatefulWidget { static String routeName = "/splash"; const SplashScreen({super.key}); @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { int currentPage = 0; List<Map<String, String>> splashData = [ { "text": "Welcome to Cap, Let’s shop!", "image": "assets/images/splash_1.png" }, { "text": "We help people conect with store \naround Federative Republic of Brazil", "image": "assets/images/splash_2.png" }, { "text": "We show the easy way to shop. \nJust stay at home with us", "image": "assets/images/splash_3.png" }, ]; @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: SizedBox( width: double.infinity, child: Column( children: <Widget>[ Expanded( flex: 3, child: PageView.builder( onPageChanged: (value) { setState(() { currentPage = value; }); }, itemCount: splashData.length, itemBuilder: (context, index) => SplashContent( image: splashData[index]["image"], text: splashData[index]['text'], ), ), ), Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: <Widget>[ const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( splashData.length, (index) => AnimatedContainer( duration: kAnimationDuration, margin: const EdgeInsets.only(right: 5), height: 6, width: currentPage == index ? 20 : 6, decoration: BoxDecoration( color: currentPage == index ? kPrimaryColor : const Color(0xFFD8D8D8), borderRadius: BorderRadius.circular(3), ), ), ), ), const Spacer(flex: 3), ElevatedButton( onPressed: () { Navigator.pushNamed(context, SignInScreen.routeName); }, child: const Text("Continue"), ), const Spacer(), ], ), ), ), ], ), ), ), ); } }
->splash->components
splash_content.dart
import 'package:flutter/material.dart'; import '../../../core/constants/constants.dart'; class SplashContent extends StatefulWidget { const SplashContent({ super.key, this.text, this.image, }); final String? text, image; @override State<SplashContent> createState() => _SplashContentState(); } class _SplashContentState extends State<SplashContent> { @override Widget build(BuildContext context) { return Column( children: <Widget>[ const Spacer(), const Text( "CAP", style: TextStyle( fontSize: 32, color: kPrimaryColor, fontWeight: FontWeight.bold, ), ), Text( widget.text!, textAlign: TextAlign.center, ), const Spacer(flex: 2), Image.asset( widget.image!, height: 265, width: 235, ), ], ); } }
Lib->Src->screens
init_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:shop_app/src/core/constants/constants.dart'; import 'package:shop_app/src/screens/favorite/favorite_screen.dart'; import 'package:shop_app/src/screens/home/home_screen.dart'; import 'package:shop_app/src/screens/profile/profile_screen.dart'; const Color inActiveIconColor = Color(0xFFB6B6B6); class InitScreen extends StatefulWidget { const InitScreen({super.key}); static String routeName = "/"; @override State<InitScreen> createState() => _InitScreenState(); } class _InitScreenState extends State<InitScreen> { int currentSelectedIndex = 0; void updateCurrentIndex(int index) { setState(() { currentSelectedIndex = index; }); } final pages = [ const HomeScreen(), const FavoriteScreen(), const Center( child: Text("Chat"), ), const ProfileScreen() ]; @override Widget build(BuildContext context) { return Scaffold( body: pages[currentSelectedIndex], bottomNavigationBar: BottomNavigationBar( onTap: updateCurrentIndex, currentIndex: currentSelectedIndex, showSelectedLabels: false, showUnselectedLabels: false, type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: SvgPicture.asset( "assets/icons/Shop Icon.svg", colorFilter: const ColorFilter.mode( inActiveIconColor, BlendMode.srcIn, ), ), activeIcon: SvgPicture.asset( "assets/icons/Shop Icon.svg", colorFilter: const ColorFilter.mode( kPrimaryColor, BlendMode.srcIn, ), ), label: "Home", ), BottomNavigationBarItem( icon: SvgPicture.asset( "assets/icons/Heart Icon.svg", colorFilter: const ColorFilter.mode( inActiveIconColor, BlendMode.srcIn, ), ), activeIcon: SvgPicture.asset( "assets/icons/Heart Icon.svg", colorFilter: const ColorFilter.mode( kPrimaryColor, BlendMode.srcIn, ), ), label: "Fav", ), BottomNavigationBarItem( icon: SvgPicture.asset( "assets/icons/Chat bubble Icon.svg", colorFilter: const ColorFilter.mode( inActiveIconColor, BlendMode.srcIn, ), ), activeIcon: SvgPicture.asset( "assets/icons/Chat bubble Icon.svg", colorFilter: const ColorFilter.mode( kPrimaryColor, BlendMode.srcIn, ), ), label: "Chat", ), BottomNavigationBarItem( icon: SvgPicture.asset( "assets/icons/User Icon.svg", colorFilter: const ColorFilter.mode( inActiveIconColor, BlendMode.srcIn, ), ), activeIcon: SvgPicture.asset( "assets/icons/User Icon.svg", colorFilter: const ColorFilter.mode( kPrimaryColor, BlendMode.srcIn, ), ), label: "Fav", ), ], ), ); } }
Lib->Src
routes.dart
import 'package:flutter/widgets.dart'; import 'package:shop_app/src/screens/products/products_screen.dart'; import 'screens/cart/cart_screen.dart'; import 'screens/complete_profile/complete_profile_screen.dart'; import 'screens/details/details_screen.dart'; import 'screens/forgot_password/forgot_password_screen.dart'; import 'screens/home/home_screen.dart'; import 'screens/init_screen.dart'; import 'screens/login_success/login_success_screen.dart'; import 'screens/otp/otp_screen.dart'; import 'screens/profile/profile_screen.dart'; import 'screens/sign_in/sign_in_screen.dart'; import 'screens/sign_up/sign_up_screen.dart'; import 'screens/splash/splash_screen.dart'; // We use name route // All our routes will be available here final Map<String, WidgetBuilder> routes = { InitScreen.routeName: (context) => const InitScreen(), SplashScreen.routeName: (context) => const SplashScreen(), SignInScreen.routeName: (context) => const SignInScreen(), ForgotPasswordScreen.routeName: (context) => const ForgotPasswordScreen(), LoginSuccessScreen.routeName: (context) => const LoginSuccessScreen(), SignUpScreen.routeName: (context) => const SignUpScreen(), CompleteProfileScreen.routeName: (context) => const CompleteProfileScreen(), OtpScreen.routeName: (context) => const OtpScreen(), HomeScreen.routeName: (context) => const HomeScreen(), ProductsScreen.routeName: (context) => const ProductsScreen(), DetailsScreen.routeName: (context) => const DetailsScreen(), CartScreen.routeName: (context) => const CartScreen(), ProfileScreen.routeName: (context) => const ProfileScreen(), };
Lib
main.dart
import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:shop_app/src/screens/splash/splash_screen.dart'; import 'src/routes.dart'; import 'src/core/constants/theme.dart'; void main() { ErrorWidget.builder = (FlutterErrorDetails errorDatails) => CustomError(errorDatails: errorDatails); runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); }, (error, stack) { log('Erro não tratado',error: error, stackTrace: stack); throw error; }); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'The Flutter Way - Template', theme: AppTheme.lightTheme(context), initialRoute: SplashScreen.routeName, routes: routes, ); } } class CustomError extends StatelessWidget { final FlutterErrorDetails errorDatails; const CustomError({ super.key, required this.errorDatails, }); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Center( child: Container( decoration: BoxDecoration( color: Colors.redAccent.withOpacity(.3), borderRadius: BorderRadius.circular(12), ), width: 300, height: 300, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline_outlined, color: Colors.red, size: 80, ), const SizedBox( height: 10.0, ), const Text( 'Error Occurred!', style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), ), Text( kDebugMode ? 'Oops... something went wrong' : errorDatails.exception.toString(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 16.0), ), ], ), ), ), ); } }
Vídeo demonstrando o projeto
Segue o código fonte completo no meu Github (link)