Criando uma Loja Virtual em Flutter com acesso a Firebase
Estaremos mostrando um projeto de Loja virtual. Código por Código.
Iniciamos pelo inicio…. Main.dart
import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/models/cart_model.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/ui/home_screen.dart'; import 'package:lojavirtual/ui/login_screen.dart'; import 'package:lojavirtual/ui/signup_screen.dart'; import 'package:scoped_model/scoped_model.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ScopedModel<UserModel>( model: UserModel(), child: ScopedModelDescendant<UserModel>( builder: (context,child,model) { return ScopedModel<CartModel>( model: CartModel(model), child: MaterialApp( title: 'Loja Flutter', theme: ThemeData( primarySwatch: Colors.blue, primaryColor: Color.fromARGB(255, 4, 125, 141) ), debugShowCheckedModeBanner: false, home: HomeScreen(), ), ); } ), ); } }
Nele iniciamos a conexão do Firebase, e start do app, no fim do projeto irei listar todos os componentes utilizados para este projeto. Além de ter acesso ao Firebase (que é gratuito).
Logo no inicio do app antes de abrir outra tela, carregamos o UserModel e o CardModel com o medelo.
user_model.dart
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; class UserModel extends Model { FirebaseAuth _auth = FirebaseAuth.instance; User? firebaseUser; Map<String,dynamic> userData = Map(); bool isLoading = false; static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); @override void addListener(VoidCallback listener) { super.addListener(listener); _loadCurrentUser(); } void signUp({required Map<String, dynamic> userData, required String pass, required VoidCallback onSuccess, required VoidCallback onFail}) { isLoading = true; notifyListeners(); _auth.createUserWithEmailAndPassword( email: userData['email'], password: pass ).then((user) async { firebaseUser = user.user; await _saveUserData(userData); onSuccess(); isLoading = false; notifyListeners(); }).catchError((e){ onFail(); isLoading = false; notifyListeners(); }); } Future<void> signIn({required String email, required String pass, required VoidCallback onSuccess, required VoidCallback onFail}) async { isLoading = true; notifyListeners(); _auth.signInWithEmailAndPassword(email: email, password: pass).then((user) async { firebaseUser = user.user; await _loadCurrentUser(); onSuccess(); isLoading = false; notifyListeners(); }).catchError((onError) { onFail(); isLoading = false; notifyListeners(); }); } Future<void> recoverPass(String email) async { _auth.sendPasswordResetEmail(email: email); } bool isLoggedIn() { return firebaseUser != null; } Future<Null> _saveUserData(Map<String, dynamic> userData) async { this.userData = userData; await FirebaseFirestore.instance .collection("users") .doc(firebaseUser?.uid) .set(userData); } Future<void> siginOut() async { await _auth.signOut(); userData = Map(); firebaseUser = null; notifyListeners(); } Future<Null> _loadCurrentUser() async { if(firebaseUser == null) { firebaseUser = await _auth.currentUser; } if (firebaseUser != null) { if (userData["name"] == null) { DocumentSnapshot docUser = await FirebaseFirestore.instance .collection("users") .doc(firebaseUser!.uid) .get(); userData = docUser.data() as Map<String, dynamic>; } } notifyListeners(); } }
Dentro dele encontra diversos métodos e funções para acesso aos dados do Usuário e acesso.
cart_model.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/datas/cart_product.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:scoped_model/scoped_model.dart'; class CartModel extends Model { UserModel? user; List<CartProduct> products = []; bool isLoading = false; String? couponCode; int discountPercentage = 0; int prazo = 0; double preco = 0; String _users="users"; String _cart ="cart"; CartModel(this.user) { if(user!.isLoggedIn()) _loadCartItems(); } static CartModel of(BuildContext context) => ScopedModel.of<CartModel>(context); void addCartItem(CartProduct cartProduct) { products.add(cartProduct); FirebaseFirestore.instance .collection(_users) .doc(user?.firebaseUser?.uid) .collection(_cart) .add(cartProduct.toMap()).then((doc) { cartProduct.cid = doc.id; }); notifyListeners(); } void removeCartItem(CartProduct cartProduct) { FirebaseFirestore.instance .collection(_users) .doc(user?.firebaseUser?.uid) .collection(_cart) .doc(cartProduct.cid).delete(); products.remove(cartProduct); notifyListeners(); } void decProduct(CartProduct cartProduct){ if(cartProduct != null) cartProduct.quantity--; FirebaseFirestore.instance .collection(_users).doc(user?.firebaseUser?.uid) .collection(_cart) .doc(cartProduct.cid).update(cartProduct.toMap()); notifyListeners(); } void incProduct(CartProduct cartProduct){ cartProduct.quantity++; FirebaseFirestore.instance .collection(_users).doc(user?.firebaseUser?.uid) .collection(_cart) .doc(cartProduct.cid).update(cartProduct.toMap()); notifyListeners(); } void setCoupon(String couponCode, int discountPercentage){ this.couponCode = couponCode; this.discountPercentage = discountPercentage; } void updatePrices(){ notifyListeners(); } void setShip(int prazo, double preco){ this.prazo = prazo; this.preco = preco; } double getProductsPrice(){ double price = 0.0; for(CartProduct c in products){ final productData = c.productData; if(productData != null) { price += c.quantity * productData.price; } } return price; } double getDiscount(){ return getProductsPrice() * discountPercentage / 100; } double? getShipPrice(){ if(preco > 0) return preco; else return 9.99; } Future<String?> finishOrder() async { if(products.length == 0) return null; isLoading = true; notifyListeners(); double productsPrice = getProductsPrice(); double? shipPrice = getShipPrice(); double discount = getDiscount(); DocumentReference refOrder = await FirebaseFirestore.instance.collection("orders").add( { "clientId": user?.firebaseUser?.uid, "products": products.map((cartProduct)=>cartProduct.toMap()).toList(), "shipPrice": shipPrice, "productsPrice": productsPrice, "discount": discount, "totalPrice": productsPrice - discount + shipPrice!, "status": 1 } ); await FirebaseFirestore.instance.collection("users").doc(user?.firebaseUser?.uid) .collection("orders").doc(refOrder.id).set( { "orderId": refOrder.id } ); QuerySnapshot query = await FirebaseFirestore.instance.collection("users").doc(user?.firebaseUser?.uid) .collection("cart").get(); for(DocumentSnapshot doc in query.docs){ doc.reference.delete(); } products.clear(); couponCode = null; discountPercentage = 0; isLoading = false; notifyListeners(); return refOrder.id; } void _loadCartItems() async { QuerySnapshot query = await FirebaseFirestore.instance.collection(_users).doc(user?.firebaseUser?.uid) .collection(_cart) .get(); products = query.docs.map((doc) => CartProduct.fromDocuments(doc)).toList(); notifyListeners(); } }
Dentro dele encontra diversos métodos e funções para acesso aos dados do Carrinho de compras.
Logo após ele carregar o modelo, ele chama a tela HomeScreen(), no home_screen.dart
home_screen.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/tabs/category_tab.dart'; import 'package:lojavirtual/tabs/home_tab.dart'; import 'package:lojavirtual/tabs/orders_tab.dart'; import 'package:lojavirtual/tabs/places_tab.dart'; import 'package:lojavirtual/widgets/card_button.dart'; import 'package:lojavirtual/widgets/custom_drawer.dart'; class HomeScreen extends StatelessWidget { HomeScreen({Key? key}) : super(key: key); final _pageController = PageController(); @override Widget build(BuildContext context) { return PageView( controller: _pageController, physics: NeverScrollableScrollPhysics(), children: <Widget>[ Scaffold( body: HomeTab(), drawer: CustomDrawer(_pageController), floatingActionButton: CardButton(), ), Scaffold( appBar: AppBar( title: Text("Categorias"), centerTitle: true, ), body: CategoryTab(), drawer: CustomDrawer(_pageController), floatingActionButton: CardButton(), ), Scaffold( appBar: AppBar( title: Text("Lojas"), centerTitle: true, ), body: PlacesTab(), drawer: CustomDrawer(_pageController), ), Scaffold( appBar: AppBar( title: Text("Meus Pedidos"), centerTitle: true, ), body: OrdersTab(), drawer: CustomDrawer(_pageController), ) ], ); } }
Dentro deste, encontra-se os diversos funções com ‘tabs’ como HomeTab, CategoryTab, PlacesTab, OrderTab, sendo que a iniciada por padrão é a HomeTab, as outras são acessadas da função drawer via o arquivo CustomDrawer.
home_tab.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:transparent_image/transparent_image.dart'; class HomeTab extends StatelessWidget { const HomeTab({Key? key}) : super(key: key); @override Widget build(BuildContext context) { Widget _buildBodyBack() => Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color.fromARGB(255, 211, 110, 130), Color.fromARGB(255, 253, 101, 100), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ); return Stack( children: [ _buildBodyBack(), CustomScrollView( slivers: [ const SliverAppBar( floating: true, snap: true, backgroundColor: Colors.transparent, elevation: 0, flexibleSpace: FlexibleSpaceBar( title: Text("Noticias"), centerTitle: true, ), ), FutureBuilder<QuerySnapshot>( future: FirebaseFirestore.instance .collection("home") .orderBy("pos") .get(), builder: (context, snapshot) { if(!snapshot.hasData) { return SliverToBoxAdapter( child: Container( height: 200.0, alignment: Alignment.center, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation<Color>(Colors.white), ), ), ); } else { return SliverMasonryGrid.count( crossAxisCount: 2, mainAxisSpacing: 1.0, crossAxisSpacing: 1.0, itemBuilder: (context, index) { return FadeInImage.memoryNetwork( placeholder: kTransparentImage, image: snapshot.data!.docs[index]['image'], fit: BoxFit.cover ); }, childCount: snapshot.data!.docs.length, ); } } )], ), ], ); } }
Nesta tela você usará para baixar as (Noticias ou nome que desejar) com imagens vindo do backend (Firebase Database), carregado da coleção ‘home’, montando as telas imagem por imagem.
No customdrawer, tem o gerenciamento das atividades do menu drawer, carregando a inicial (Home), e as outras 3 telas.
custom_drawer.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/tiles/drawer_tile.dart'; import 'package:lojavirtual/ui/login_screen.dart'; import 'package:scoped_model/scoped_model.dart'; class CustomDrawer extends StatelessWidget { final PageController pageController; CustomDrawer(this.pageController, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { Widget _buildDrawerBack() => Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color.fromARGB(255, 203, 236, 241), Colors.white, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ); return Drawer( child: Stack( children: <Widget>[ _buildDrawerBack(), ListView( padding: EdgeInsets.only(left: 32.0, top: 16.0), children: [ Container( margin: EdgeInsets.only(bottom: 8.0), padding: EdgeInsets.fromLTRB(0.0, 16.0, 16.0, 8.0), height: 170.0, child: Stack( children: <Widget>[ Positioned( top: 0.0, left: 0.0, child: Text( "Loja Virtual\nProvisoria", style: TextStyle( fontSize: 34.0, fontWeight: FontWeight.bold), )), Positioned( left: 0.0, bottom: 0.0, child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Olá, ${!model.isLoggedIn() ? "" : model.userData['name']}", style: TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold), ), GestureDetector( child: Text( !model.isLoggedIn() ? "Entre ou cadastre-se >," : "Sair", style: TextStyle( fontSize: 16.0, color: Theme .of(context) .primaryColor, fontWeight: FontWeight.bold, ), ), onTap: () { if(!model.isLoggedIn()) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => LoginScreen()) ); } else { model.siginOut(); } }, ), ], ); } )) ], ), ), Divider(), DrawerTile(Icons.home, "Inicio", pageController,0), DrawerTile(Icons.list, "Categorias", pageController,1), DrawerTile(Icons.location_on, "Lojas", pageController,2), DrawerTile(Icons.playlist_add_check, "Meus Pedidos", pageController,3), ], ) ], ), ); } }
Dentro deste arquivo se encontra o dados do usuario ou solicitação para Login, caso não esteja logado, ao clicar em Enter ou Cadastre-se, ele chama a LoginScreen
login_screen.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/ui/signup_screen.dart'; import 'package:scoped_model/scoped_model.dart'; class LoginScreen extends StatefulWidget { LoginScreen({Key? key}) : super(key: key); @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { final _emailController = TextEditingController(); final _passController = TextEditingController(); final _formKey = GlobalKey<FormState>(); final _scaffoldKey = GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, appBar: AppBar( title: Text("Entrar"), centerTitle: true, actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => SignupScreen())); }, child: Text( "Criar Conta", style: TextStyle(fontSize: 16.0), ), ) ], ), body: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) return Center( child: CircularProgressIndicator(), ); return Form( key: _formKey, child: ListView( padding: EdgeInsets.all(16.0), children: <Widget>[ TextFormField( controller: _emailController, decoration: InputDecoration(hintText: 'E-mail'), keyboardType: TextInputType.emailAddress, validator: (text) { if (text!.isEmpty || !text.contains("@")) return "E-mail invalido!"; }, ), SizedBox( height: 16.0, ), TextFormField( controller: _passController, decoration: InputDecoration(hintText: 'Senha'), obscureText: true, validator: (text) { if (text!.isEmpty || text.length < 6) return "Senha invalida!"; }, ), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { if(_emailController.text.isEmpty) SnackBar(content: Text("Insira seu email para recuperação!"), backgroundColor: Colors.redAccent, duration: Duration(seconds: 2), ); else { model.recoverPass(_emailController.text); SnackBar(content: Text("Confire seu email!"), backgroundColor: Theme .of(context) .primaryColor, duration: Duration(seconds: 2), ); } }, child: Text( "Esqueci minha senha", textAlign: TextAlign.right, style: TextStyle(fontSize: 16.0), ), ), ), SizedBox( height: 16.0, ), SizedBox( height: 44.0, child: ElevatedButton( style: raisedButtonStyle, child: Text( "Entrar", style: TextStyle( fontSize: 18.0, ), ), onPressed: () { if (_formKey.currentState!.validate()) {} model.signIn( email: _emailController.text, pass: _passController.text, onSuccess: () => _onSuccess(), onFail: () => _onFail(), ); }), ) ], ), ); }), ); } final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.blue.shade400, textStyle: TextStyle(color: Colors.white), minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2)), ), ); final ButtonStyle flatButtonStyle = TextButton.styleFrom( foregroundColor: Colors.white, minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16.0), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2.0)), ), ); void _onSuccess() { Navigator.of(context).pop(); } void _onFail() { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Falha ao criar entrar!"), backgroundColor: Colors.redAccent, duration: Duration(seconds: 2), ) ); } }
Está tela carrega o Model de User, para efetuar as atividades de Login, ao clicar em Criar Conta, ele ira direcionar para a tela SignUpScreen, efetuando o cadastro, e caso se logue ao finalizar ele direciona para a tela home.
signup_screen.dart
import 'dart:math'; import 'package:flutter/material.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:scoped_model/scoped_model.dart'; class SignupScreen extends StatefulWidget { SignupScreen({Key? key}) : super(key: key); @override State<SignupScreen> createState() => _SignupScreenState(); } class _SignupScreenState extends State<SignupScreen> { final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _passController = TextEditingController(); final _addressController = TextEditingController(); final _formKey = GlobalKey<FormState>(); final _scaffoldKey = GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, appBar: AppBar( title: Text("Criar Conta"), centerTitle: true, ), body: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if(model.isLoading) return Center(child: CircularProgressIndicator(),); return Form( key: _formKey, child: ListView( padding: EdgeInsets.all(16.0), children: <Widget>[ TextFormField( controller: _nameController, decoration: InputDecoration( hintText: 'Nome Completo' ), keyboardType: TextInputType.name, validator: (text) { if (text!.isEmpty || text.length < 10) return "Nome invalido!"; }, ), SizedBox(height: 16.0,), TextFormField( controller: _addressController, decoration: InputDecoration( hintText: 'Endereço' ), keyboardType: TextInputType.streetAddress, validator: (text) { if (text!.isEmpty || text.length < 10) return "Endereço invalida!"; }, ), SizedBox(height: 16.0,), TextFormField( controller: _emailController, decoration: InputDecoration( hintText: 'E-mail' ), keyboardType: TextInputType.emailAddress, validator: (text) { if (text!.isEmpty || !text.contains("@")) return "E-mail invalido!"; }, ), SizedBox(height: 16.0,), TextFormField( controller: _passController, decoration: InputDecoration( hintText: 'Senha' ), obscureText: true, validator: (text) { if (text!.isEmpty || text.length < 6) return "Senha invalida!"; }, ), SizedBox(height: 16.0,), SizedBox( height: 44.0, child: ElevatedButton( style: raisedButtonStyle, child: Text("Criar Conta", style: TextStyle(fontSize: 18.0,), ), onPressed: () { if (_formKey.currentState!.validate()) { Map<String, dynamic> userData = { "name": _nameController.text, "email": _emailController.text, "address": _addressController.text }; model.signUp( userData: userData, pass: _passController.text, onSuccess: _onSuccess, onFail: _onFail, ); } } ), ) ], ), ); } ), ); } final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.blue.shade400, textStyle: TextStyle(color: Colors.white), minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2)), ), ); void _onSuccess() { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Usuário criado com sucesso!"), backgroundColor: Theme.of(context).primaryColor, duration: Duration(seconds: 2), ) ); Future.delayed(Duration(seconds: 2)).then((_) { Navigator.of(context).pop(); }); } void _onFail() { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Falha ao criar usuário!"), backgroundColor: Colors.redAccent, duration: Duration(seconds: 2), ) ); } }
Nesta tela você efetua seu cadastro no Firebase, e se loga automaticamente, indo para a Home.
No drawertile você deseja cada (função/opção) da drawer.
drawer_tile.dart
import 'package:flutter/material.dart'; class DrawerTile extends StatelessWidget { final IconData icon; final String text; final PageController controller; final int page; DrawerTile(this.icon, this.text, this.controller, this.page, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: InkWell( onTap: () { Navigator.of(context).pop(); controller.jumpToPage(page); }, child: Container( height: 60.0, child: Row( children: <Widget>[ Icon( icon, size: 32.0, color: controller.page!.round() == page ? Theme.of(context).primaryColor : Colors.grey[700], ), SizedBox(width: 32.0,), Text( text, style: TextStyle( fontSize: 16.0, color: controller.page == page ? Theme.of(context).primaryColor : Colors.grey[700], ), ) ], ), ), ), ); } }
Ao selecionar a tela de Category você direciona para o tab chamada CategoryTab
category_tab.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/tiles/category_tile.dart'; class CategoryTab extends StatelessWidget { const CategoryTab({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return FutureBuilder<QuerySnapshot>( future: FirebaseFirestore.instance .collection("products") .get(), builder: (context, snapshot) { if(!snapshot.hasData) return Center(child: CircularProgressIndicator(),); else { var dividedTiles = ListTile.divideTiles( tiles: snapshot.data!.docs.map( (doc) { return CategoryTile(doc); } ).toList(), color: Colors.grey[500]) .toList(); return ListView( children: dividedTiles ); } } ); } }
Nesta tela listas as categorias vindas do Firebase, com suas respectivas imagens, texto etc. Ao selecionar uma categoria ele é direcionada para a tela categorytile.
category_tile.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/ui/category_screen.dart'; class CategoryTile extends StatelessWidget { final DocumentSnapshot snapshot; CategoryTile(this.snapshot, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( radius: 25.0, backgroundColor: Colors.transparent, backgroundImage: NetworkImage(snapshot.get('icon'),) ), title: Text(snapshot.get('title')), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context)=>CategoryScreen(snapshot)) ); }, ); } }
Dentro desta função ele ira carregar a CategoryScreen com a tela para apresentar os dados do Firebase.
category_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/datas/product_data.dart'; import 'package:lojavirtual/tiles/product_tile.dart'; class CategoryScreen extends StatelessWidget { final DocumentSnapshot snapshot; CategoryScreen(this.snapshot, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: Text(snapshot.get('title')), centerTitle: true, bottom: TabBar( indicatorColor: Colors.white, tabs: <Widget>[ Tab(icon: Icon(Icons.grid_on),), Tab(icon: Icon(Icons.list),) ], ), ), body: FutureBuilder<QuerySnapshot>( future: FirebaseFirestore.instance .collection("products").doc(snapshot.id).collection('itens') .get(), builder: (context, snapshot) { if (!snapshot.hasData) { return Center(child: CircularProgressIndicator()); } else { return TabBarView( physics: NeverScrollableScrollPhysics(), children: [ GridView.builder( padding: EdgeInsets.all(4.0), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 4.0, crossAxisSpacing: 4.0, childAspectRatio: 0.65, ), itemCount: snapshot.data!.docs.length, itemBuilder: (context, index) { ProductData data = ProductData.fromDocument(snapshot.data!.docs[index]); data.category = this.snapshot.id; return ProductTile("grid", data); }, ), ListView.builder( padding: EdgeInsets.all(4.0), itemCount: snapshot.data!.docs.length, itemBuilder: (context, index) { ProductData data = ProductData.fromDocument(snapshot.data!.docs[index]); data.category = this.snapshot.id; return ProductTile("list", data); } ) ], ); } }, ), ), ); } }
Ao receber os dados ele ira abrir ao arquivo chamado producttile, com duas opções (Grid ou List). abas estão no mesmo.
product_tile.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/datas/product_data.dart'; import 'package:lojavirtual/ui/product_screen.dart'; class ProductTile extends StatelessWidget { final String type; final ProductData data; ProductTile(this.type, this.data, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => ProductScreen(data)) ); }, child: Card( child: type == "grid" ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ AspectRatio( aspectRatio: 0.8, child: Image.network( data.images![0], fit: BoxFit.cover, ), ), Expanded( child: Container( padding: EdgeInsets.all(0.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Padding( padding: const EdgeInsets.only(left: 2.0, bottom: 1.0), child: Text(data.title!, style: TextStyle( fontWeight: FontWeight.w500 ), ), ), Padding( padding: const EdgeInsets.only(left: 2.0), child: Text( "R\$ ${data.price.toStringAsFixed(2)}", style: TextStyle( color: Theme.of(context).primaryColor, fontSize: 17.0, fontWeight: FontWeight.bold ), ), ), ], ), ), ) ], ) : Row( children: <Widget>[ Flexible( flex: 1, child: Image.network( data.images![0], fit: BoxFit.cover, height: 250.0, ), ), Flexible( flex: 2, child: Container( padding: EdgeInsets.all(0.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Padding( padding: const EdgeInsets.only(left: 2.0, bottom: 1.0), child: Text(data.title!, style: TextStyle( fontWeight: FontWeight.w500 ), ), ), Padding( padding: const EdgeInsets.only(left: 2.0), child: Text( "R\$ ${data.price.toStringAsFixed(2)}", style: TextStyle( color: Theme.of(context).primaryColor, fontSize: 17.0, fontWeight: FontWeight.bold ), ), ), ], ), ), ) ], ) ), ); } }
Ao clicar no produto ele carrega a tela productscreen, com os dados da tala.
product_screen.dart
import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/datas/cart_product.dart'; import 'package:lojavirtual/datas/product_data.dart'; import 'package:lojavirtual/models/cart_model.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/ui/cart_screen.dart'; import 'package:lojavirtual/ui/login_screen.dart'; class ProductScreen extends StatefulWidget { final ProductData data; ProductScreen(this.data, {Key? key}) : super(key: key); int _current = 0; final CarouselController _carouselController = CarouselController(); @override State<ProductScreen> createState() => _ProductScreenState(data); } class _ProductScreenState extends State<ProductScreen> { final ProductData data; String size=""; int _current = 0; final CarouselController _carouselController = CarouselController(); _ProductScreenState(this.data); @override Widget build(BuildContext context) { final Color primeryColor = Theme.of(context).primaryColor; return Scaffold( appBar: AppBar( title: Text(data.title!), centerTitle: true, ), body: ListView( children: <Widget>[ AspectRatio( aspectRatio: 0.9, child: CarouselSlider( carouselController: _carouselController, options: CarouselOptions( enlargeCenterPage: true, autoPlay: true, aspectRatio: 1.0, enlargeStrategy: CenterPageEnlargeStrategy.height, onPageChanged: (index, reason) { setState(() { _current = index; }); }, ), items: data.images?.map((e) { return Image.network(e, fit: BoxFit.cover); }).toList(), ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: data.images!.asMap().entries.map((entry) { return GestureDetector( onTap: () => _carouselController.animateToPage(entry.key), child: Container( width: 8.0, height: 8.0, margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), decoration: BoxDecoration( shape: BoxShape.circle, color: (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black) .withOpacity(_current == entry.key ? 0.9 : 0.4)), ), ); }).toList(), ), Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Text( data.title!, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, ), maxLines: 3, ), Text( "R\$ ${data.price.toStringAsFixed(2)}", style: TextStyle( fontSize: 22.0, fontWeight: FontWeight.bold, color: primeryColor ), ), SizedBox(height: 16.0,), Text( "Tamanho", style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500 ), ), SizedBox( height: 34.0, child: GridView( padding: EdgeInsets.symmetric(vertical: 4.0), scrollDirection: Axis.horizontal, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 1, mainAxisSpacing: 8.0, childAspectRatio: 0.5, ), children: data.sizes!.map((s) { return GestureDetector( onTap: () { setState(() { size = s; }); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(4.0)), border: Border.all( color: s == size ? primeryColor : Colors.grey.shade500, width: 3.0 ), ), width: 50.0, alignment: Alignment.center, child: Text(s), ), ); }).toList(), ), ), SizedBox(height: 16.0,), SizedBox( height: 44.0, child: ElevatedButton( onPressed: size != "" ? () { if(UserModel.of(context).isLoggedIn()) { CartProduct cartProduct = CartProduct(); cartProduct.size = size; cartProduct.quantity = 1; cartProduct.pid = data.id; cartProduct.productData = data; cartProduct.category = data.category; CartModel.of(context).addCartItem(cartProduct); Navigator.of(context).push( MaterialPageRoute(builder: (context)=>CartScreen()) ); } else { Navigator.of(context).push( MaterialPageRoute(builder: (context)=>LoginScreen()) ); } } : null, child: Text(UserModel.of(context).isLoggedIn() ? "Adicionar ao Carrinho" : "Entre para Comprar!", style: TextStyle( fontSize: 18.0 ), ), style: raisedButtonStyle, ), ), SizedBox(height: 16.0,), Text( "Descrição", style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500 ), ), Text( data.discription!, style: TextStyle( fontSize: 16.0, ), ), ], ), ) ], ), ); } final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.blue.shade400, textStyle: TextStyle(color: Colors.white), minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2)), ), ); }
Ao escolher um item da lista ele ira carregar a tela de carrinho de compras, chamada CartScreen
cart_screen.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/models/cart_model.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/tiles/cart_tile.dart'; import 'package:lojavirtual/ui/login_screen.dart'; import 'package:lojavirtual/ui/order_screen.dart'; import 'package:lojavirtual/widgets/cart_price.dart'; import 'package:lojavirtual/widgets/custom_mensagem.dart'; import 'package:lojavirtual/widgets/discount_card.dart'; import 'package:lojavirtual/widgets/ship_cart.dart'; import 'package:scoped_model/scoped_model.dart'; class CartScreen extends StatelessWidget { const CartScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Meu Carrinho"), actions: [ Container( padding: EdgeInsets.only(right: 8.0), alignment: Alignment.center, child: ScopedModelDescendant<CartModel>( builder: (context, child, model) { int p = model.products.length; return Text( "${p != 0 ? p: "0"} ${p == 1 ? "ITEM" : "ITENS"}", style: TextStyle( fontSize: 17.0, ), ); } ), ) ], ), body: ScopedModelDescendant<CartModel>( builder: (context,child, model) { if(model.isLoading && UserModel.of(context).isLoggedIn()) { return Center(child: CircularProgressIndicator(),); } else if (!UserModel.of(context).isLoggedIn()) { CustomMensagem("Faça o login para adicionar produtos!",0); } else if (model.products.isEmpty && model.products.length == 0) { return Center(child: Text("Nenhum produto no Carrinho", style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), textAlign: TextAlign.center), ); } else { return ListView( children: <Widget>[ Column( children: model.products.map( (product) { return CartTile(product); } ).toList(), ), DiscountCard(), ShipCard(), CartPrice(() async { String? orderId = await model.finishOrder(); if(orderId != null) Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context)=>OrderScreen(orderId)) ); }) ], ); } return Container(); } ), ); } }
Ao estár logado, ele ira direcionar para a tela CartTile, apresentando Carrinho de compras.
cart_tile.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/datas/cart_product.dart'; import 'package:lojavirtual/datas/product_data.dart'; import 'package:lojavirtual/models/cart_model.dart'; class CartTile extends StatelessWidget { final CartProduct cartProduct; CartTile(this.cartProduct, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { Widget _buildContent() { CartModel.of(context).updatePrices(); return Row( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ Container( width: 120.0, child: Image.network( cartProduct.productData?.images![0], fit: BoxFit.cover, ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( cartProduct.productData?.title ?? "", style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17.0), ), Text( "Tamanho: ${cartProduct.size}", style: const TextStyle(fontWeight: FontWeight.w300), ), Text( "R\$ ${cartProduct.productData?.price?.toStringAsFixed(2)}", style: TextStyle( color: Theme.of(context).primaryColor, fontSize: 16.0, fontWeight: FontWeight.bold ) ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ IconButton( onPressed: cartProduct.quantity > 1 ? () { CartModel.of(context).decProduct(cartProduct); } : null, icon: Icon(Icons.remove, color: Theme.of(context).primaryColor,) ), Text(cartProduct.quantity.toString()), IconButton( onPressed: () { CartModel.of(context).incProduct(cartProduct); }, icon: Icon(Icons.add, color: Theme.of(context).primaryColor) ), TextButton( onPressed: () { CartModel.of(context).removeCartItem(cartProduct); }, style: flatButtonStyle, child: Text("Remover"), ), ], ) ], ) ) ], ); } return Card( margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: cartProduct.productData == null ? FutureBuilder<DocumentSnapshot>( future: FirebaseFirestore.instance .collection("products").doc(cartProduct.category).collection('itens') .doc(cartProduct.pid).get(), builder: (context, snapshot) { if (snapshot.hasData) { cartProduct.productData = ProductData.fromDocument(snapshot.data); return _buildContent(); } else { return Container( height: 70.0, child: CircularProgressIndicator(), alignment: Alignment.center, ); } } ) : _buildContent() ); } final ButtonStyle flatButtonStyle = TextButton.styleFrom( foregroundColor: Colors.grey.shade500, minimumSize: Size(88, 36), padding: EdgeInsets.zero, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2.0)), ), ); }
Bem como os outros componentes do carrinho de compra como discountCard, ShipCard, CardPrice que ao confirmar e direcionado para a tela OrderScreen.
discount_card.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:lojavirtual/models/cart_model.dart'; class DiscountCard extends StatelessWidget { const DiscountCard({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.symmetric(horizontal: 8.0,vertical: 4.0), child: ExpansionTile( title: Text("Cupom de Desconto", textAlign: TextAlign.start, style: TextStyle( fontWeight: FontWeight.w500, color: Colors.grey.shade700 ), ), leading: Icon(Icons.card_giftcard), trailing: Icon(Icons.add), children: <Widget>[ Padding( padding: EdgeInsets.all(0.0), child: TextFormField( decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Digite seu cupom" ), initialValue: CartModel.of(context).couponCode ?? "", onFieldSubmitted: (text) { FirebaseFirestore.instance.collection("coupons").doc(text).get().then((docSnap) { if(docSnap.data() != null) { CartModel.of(context).setCoupon(text,docSnap.data()!['percent']); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Desconto de ${docSnap.data()!['percent']}% aplicado"), backgroundColor: Theme.of(context).primaryColor, duration: Duration(seconds: 2), ) ); } else { CartModel.of(context).setCoupon("",0); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Cupom não existente"), backgroundColor: Colors.redAccent, duration: Duration(seconds: 2), ) ); } }); }, ), ) ], ), ); } }
ship_card.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:lojavirtual/correios/correios_frete.dart'; import 'package:lojavirtual/correios/via_cep_service.dart'; import 'package:lojavirtual/models/cart_model.dart'; import 'package:xml2json/xml2json.dart'; import 'package:http/http.dart' as http; class ShipCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: ExpansionTile( title: Text( "Cálcular Frete", textAlign: TextAlign.start, style: TextStyle( fontWeight: FontWeight.w500, color: Colors.grey[700] ), ), leading: Icon(Icons.location_on), children: <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Digite seu CEP" ), initialValue: "", onFieldSubmitted: (cepDigitado) async { String _result; final resultCep = await ViaCepService.fetchCep(cep: cepDigitado); print(resultCep.localidade); // Exibindo somente a localidade no terminal _result = resultCep.toJson(); Xml2Json xml2json = new Xml2Json(); // class parse XML to JSON try { var url = Uri.parse( "http://ws.correios.com.br/calculador/CalcPrecoPrazo.aspx?nCdEmpresa=&sDsSenha=&sCepOrigem=21360230&sCepDestino=$cepDigitado&nVlPeso=1&nCdFormato=1&nVlComprimento=20&nVlAltura=20&nVlLargura=20&sCdMaoPropria=n&nVlValorDeclarado=0&sCdAvisoRecebimento=n&nCdServico=04510&nVlDiametro=0&StrRetorno=xml&nIndicaCalculo=3" ); http.Response reponse = await http.get(url); print("GET DO XML"); print(reponse.body); if (reponse.statusCode == 200) { xml2json.parse(reponse.body); var resultMap = xml2json.toGData(); Correios correios = Correios.fromJson( json.decode(resultMap)["Servicos"]["cServico"]); CartModel.of(context).setShip(int.parse(correios.prazo), double.parse(correios.valor.replaceAll(",", "."))); ScaffoldMessenger.of(context).showSnackBar( SnackBar( duration: Duration(seconds: 3), content: Text( "R\$ ${correios.valor} reais \nPrazo da entrega: ${correios.prazo} dias"), backgroundColor: Theme.of(context).primaryColor, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Erro de conexão: ${reponse.statusCode}"), backgroundColor: Colors.redAccent, ), ); } } catch (erro) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(erro.toString()), backgroundColor: Colors.redAccent, ), ); } }, ), ) ], ), ); } }
Nesta tela existe 2 componentes especiais um usado, 100% e outro de teste do grupo via_cep.
correios_frete.dart
import 'package:flutter/material.dart'; class Correios { late String codigo; late String valor; late String prazo; late String entregaDomiciliar; String altura = "5"; String largura = "20"; String comprimento = "20"; Correios.fromJson(Map<String, dynamic> json) { codigo = json["Codigo"]["\$t"]; valor = json["Valor"]["\$t"]; prazo = json["PrazoEntrega"]["\$t"]; entregaDomiciliar = json["EntregaDomiciliar"]["\$t"]; } Map<String, dynamic> toJson() { return { 'Codigo': codigo, 'Valor': valor, 'PrazoEntrega': prazo, 'EntregaDomiciliar': entregaDomiciliar, }; } }
O outro é o via_cep
via_cep_service.dart
import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:lojavirtual/correios/result_cep.dart'; class ViaCepService { static Future<ResultCep> fetchCep({String? cep}) async { var url = Uri.parse( 'https://viacep.com.br/ws/$cep/json/' ); http.Response response = await http.get(url); if (response.statusCode == 200) { return ResultCep.fromJson(response.body); } else { throw Exception('Requisição inválida!'); } } }
e seu retorno
result_cep.dart
import 'dart:convert'; class ResultCep { String? cep; String? logradouro; String? complemento; String? bairro; String? localidade; String? uf; String? unidade; String? ibge; String? gia; ResultCep({ this.cep, this.logradouro, this.complemento, this.bairro, this.localidade, this.uf, this.unidade, this.ibge, this.gia, }); factory ResultCep.fromJson(String str) => ResultCep.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); factory ResultCep.fromMap(Map<String, dynamic> json) => ResultCep( cep: json["cep"], logradouro: json["logradouro"], complemento: json["complemento"], bairro: json["bairro"], localidade: json["localidade"], uf: json["uf"], unidade: json["unidade"], ibge: json["ibge"], gia: json["gia"], ); Map<String, dynamic> toMap() => { "cep": cep, "logradouro": logradouro, "complemento": complemento, "bairro": bairro, "localidade": localidade, "uf": uf, "unidade": unidade, "ibge": ibge, "gia": gia, }; }
card_price.dart
import 'package:flutter/material.dart'; import 'package:lojavirtual/models/cart_model.dart'; import 'package:scoped_model/scoped_model.dart'; class CartPrice extends StatelessWidget { final VoidCallback buy; CartPrice(this.buy, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Container( padding: EdgeInsets.all(16.0), child: ScopedModelDescendant<CartModel>( builder: (context, child, model) { double price = model.getProductsPrice(); double discount = model.getDiscount(); double? ship = model.getShipPrice(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Text( "Resumo do Pedido", textAlign: TextAlign.start, style: TextStyle( fontWeight: FontWeight.w500), ), SizedBox(height: 12.0,), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("Subtotal"), Text("R\$ ${price.toStringAsFixed(2)}") ], ), Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("Desconto"), Text("R\$ -${discount.toStringAsFixed(2)}") ], ), Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("Entrega"), Text("R\$ ${ship?.toStringAsFixed(2)}"), ] ), Divider(), SizedBox(height: 12.0,), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("Total", style: TextStyle( fontWeight: FontWeight.w500, color: Theme.of(context).primaryColor ), ), Text("R\$ ${(price + ship! - discount).toStringAsFixed(2)}", style: TextStyle( fontWeight: FontWeight.w500, fontSize: 16.0, color: Theme.of(context).primaryColor ), ) ], ), SizedBox(height: 12.0,), ElevatedButton( style: raisedButtonStyle, child: Text("Finalizar pedido"), onPressed: buy ) ], ); }, ), ), ); } final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.pink.shade400, textStyle: TextStyle(color: Colors.white), minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2)), ), ); }
Ao confirmar ela é direcionada para a tela OrderScreen, fechando o fluxo.
order_screen.dart
import 'package:flutter/material.dart'; class OrderScreen extends StatelessWidget { final String orderId; OrderScreen(this.orderId); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Pedido Realizado"), centerTitle: true, ), body: Container( padding: EdgeInsets.all(16.0), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Icon(Icons.check, color: Theme.of(context).primaryColor, size: 80.0, ), Text("Pedido realizado com sucesso!", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0), ), Text("Código do pedido: $orderId", style: TextStyle(fontSize: 16.0), ) ], ), ), ); } }
No menu drawer ainda existe mais 2 telas placestab e ordertab
Na tela placestab você uma lista de lojas carregadas no Firebase.
places_tab.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import '../tiles/place_tile.dart'; class PlacesTab extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder<QuerySnapshot>( future: FirebaseFirestore.instance.collection("places").get(), builder: (context, snapshot){ if(!snapshot.hasData) return Center( child: CircularProgressIndicator(), ); else return ListView( children: snapshot.data!.docs.map((doc) => PlaceTile(doc)).toList(), ); }, ); } }
Ao carregar ela chama a place_tile para apresentar os dados.
place_tile.dart
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class PlaceTile extends StatelessWidget { final DocumentSnapshot snapshot; PlaceTile(this.snapshot); @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ SizedBox( height: 100.0, child: Image.network( snapshot.get("image"), fit: BoxFit.cover, ), ), Container( padding: EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( snapshot.get("title"), textAlign: TextAlign.start, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 17.0 ), ), Text( snapshot.get("address"), textAlign: TextAlign.start, ) ], ), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ TextButton( style: flatButtonStyle, child: Text("Ver no Mapa"), onPressed: (){ launch("https://www.google.com/maps/search/?api=1&query=${snapshot.get("lat")}," "${snapshot.get("long")}"); }, ), TextButton( style: flatButtonStyle, child: Text("Ligar"), onPressed: (){ launch("tel:${snapshot.get("phone")}"); }, ), ], ) ], ), ); } final ButtonStyle flatButtonStyle = TextButton.styleFrom( foregroundColor: Colors.blue.shade400, minimumSize: Size(88, 36), padding: EdgeInsets.zero, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2.0)), ), ); }
Já na OrderTab, você a situação das Ordens de compra.
order_tab.dart
import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:lojavirtual/models/user_model.dart'; import 'package:lojavirtual/tiles/order_title.dart'; import 'package:lojavirtual/ui/home_screen.dart'; import 'package:lojavirtual/widgets/custom_mensagem.dart'; class OrdersTab extends StatelessWidget { OrdersTab({Key? key}) : super(key: key); @override Widget build(BuildContext context) { if(UserModel.of(context).isLoggedIn()) { String? uid = UserModel.of(context).firebaseUser?.uid; return FutureBuilder<QuerySnapshot>( future: FirebaseFirestore.instance.collection("users").doc(uid) .collection("orders").get(), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.docs.length == 0) { return Container( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Icon(Icons.add_shopping_cart, size: 80.0, color: Theme .of(context) .primaryColor), SizedBox(height: 16.0,), Text("Carrinho vazio favor voltar!", style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), SizedBox(height: 16.0,), ElevatedButton( style: raisedButtonStyle, child: Text( "Voltar", style: TextStyle(fontSize: 18.0,)), onPressed: () { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => HomeScreen())); } ) ], ), ); } else { return ListView( children: snapshot.data!.docs.map((doc) => OrderTile(doc.id)).toList() .reversed.toList(), ); } }, ); } else { return CustomMensagem("Faça o login para acompanhar!",1); } } final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.blue.shade400, textStyle: TextStyle(color: Colors.white), minimumSize: Size(88, 36), padding: EdgeInsets.symmetric(horizontal: 16), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2)), ), ); }
ela chama a order_title para apresentar as ordens.
import 'dart:collection'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; class OrderTile extends StatelessWidget { final String orderId; OrderTile(this.orderId); @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), child: Padding( padding: EdgeInsets.all(8.0), child: StreamBuilder<DocumentSnapshot>( stream: FirebaseFirestore.instance.collection("orders").doc(orderId).snapshots(), builder: (context, snapshot){ if(!snapshot.hasData) { return Center( child: CircularProgressIndicator(), ); } else { int status = snapshot.data?.get("status"); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( "Código do pedido: ${snapshot.data!.id}", style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 4.0,), Text( _buildProductsText(snapshot.data) ), SizedBox(height: 4.0,), Text( "Status do Pedido:", style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 4.0,), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ _buildCircle("1", "Preparação", status, 1), Container( height: 1.0, width: 40.0, color: Colors.grey[500], ), _buildCircle("2", "Transporte", status, 2), Container( height: 1.0, width: 40.0, color: Colors.grey[500], ), _buildCircle("3", "Entrega", status, 3), ], ) ], ); } } ), ) ); } String _buildProductsText(DocumentSnapshot? snapshot){ String text = "Descrição:\n"; for(LinkedHashMap p in snapshot?.get("products")){ text += "${p["quantity"]} x ${p["product"]["title"]} (R\$ ${p["product"]["price"].toStringAsFixed(2)})\n"; } text += "Total: R\$ ${snapshot?.get("totalPrice").toStringAsFixed(2)}"; return text; } Widget _buildCircle(String title, String subtitle, int status, int thisStatus){ Color backColor; Widget child; if(status < thisStatus){ backColor = Colors.grey.shade500; child = Text(title, style: TextStyle(color: Colors.white),); } else if (status == thisStatus){ backColor = Colors.blue; child = Stack( alignment: Alignment.center, children: <Widget>[ Text(title, style: TextStyle(color: Colors.white),), CircularProgressIndicator( valueColor: AlwaysStoppedAnimation<Color>(Colors.white), ) ], ); } else { backColor = Colors.green; child = Icon(Icons.check, color: Colors.white,); } return Column( children: <Widget>[ CircleAvatar( radius: 20.0, backgroundColor: backColor, child: child, ), Text(subtitle) ], ); } }
Ainda existe dentro do projeto alguns outros arquivos usados para guardar dados, mostrar mensagem, input_fields, etc.
Você encontrara todos e completo no meu Github (Projeto de Estudo 9 Flutter)
Em breve apresentarei a continuação o Gerenciador do mesmo.