/* For this homepage, appBar is created at the bottom after CustomScrollView we used AutomaticKeepAliveClientMixin to keep the state when moving from 1 navbar to another navbar, so the page is not refresh overtime */ import 'dart:async'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:mobdr/model/category_for_you_model.dart'; import 'package:mobdr/model/category_model.dart'; import 'package:mobdr/model/flashsale_model.dart'; import 'package:mobdr/model/home_banner_model.dart'; import 'package:mobdr/model/home_trending_model.dart'; import 'package:mobdr/model/last_search_model.dart'; import 'package:mobdr/model/recomended_product_model.dart'; import 'package:mobdr/ui/general/chat_us.dart'; import 'package:mobdr/ui/general/notification.dart'; import 'package:mobdr/ui/general/product_detail/product_detail.dart'; import 'package:mobdr/ui/home/coupon.dart'; import 'package:mobdr/ui/home/flashsale.dart'; import 'package:mobdr/ui/home/last_search.dart'; import 'package:mobdr/ui/home/product_category.dart'; import 'package:mobdr/ui/home/search.dart'; import 'package:mobdr/ui/home/search_product.dart'; import 'package:mobdr/ui/reusable/reusable_widget.dart'; import 'package:mobdr/ui/reusable/cache_image_network.dart'; import 'package:mobdr/ui/reusable/global_function.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:mobdr/config/constant.dart'; import 'package:mobdr/config/global_style.dart'; class TabHomePage extends StatefulWidget { @override _TabHomePageState createState() => _TabHomePageState(); } class _TabHomePageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { // initialize global function and reusable widget final _globalFunction = GlobalFunction(); final _reusableWidget = ReusableWidget(); int _currentImageSlider = 0; late ScrollController _scrollController; Color _topIconColor = Colors.white; Color _topSearchColor = Colors.white; late AnimationController _topColorAnimationController; late Animation _appBarColor; SystemUiOverlayStyle _appBarSystemOverlayStyle = SystemUiOverlayStyle.light; Timer? _flashsaleTimer; late int _flashsaleSecond; void _startFlashsaleTimer() { const period = const Duration(seconds: 1); _flashsaleTimer = Timer.periodic(period, (timer) { setState(() { _flashsaleSecond--; }); if (_flashsaleSecond == 0) { _cancelFlashsaleTimer(); Fluttertoast.showToast( msg: 'Flash sale is over', toastLength: Toast.LENGTH_LONG); } }); } void _cancelFlashsaleTimer() { if (_flashsaleTimer != null) { _flashsaleTimer?.cancel(); _flashsaleTimer = null; } } // keep the state to do not refresh when switch navbar @override bool get wantKeepAlive => true; @override void initState() { _setupAnimateAppbar(); // set how many times left for flashsale var timeNow = DateTime.now(); // 8000 second = 2 hours 13 minutes 20 second for flashsale timer var flashsaleTime = timeNow.add(Duration(seconds: 8000)).difference(timeNow); _flashsaleSecond = flashsaleTime.inSeconds; _startFlashsaleTimer(); super.initState(); } @override void dispose() { _scrollController.dispose(); _topColorAnimationController.dispose(); _cancelFlashsaleTimer(); super.dispose(); } void _setupAnimateAppbar() { // use this function and paramater to animate top bar _topColorAnimationController = AnimationController(vsync: this, duration: Duration(seconds: 0)); _appBarColor = ColorTween(begin: Colors.transparent, end: Colors.white) .animate(_topColorAnimationController); _scrollController = ScrollController() ..addListener(() { _topColorAnimationController.animateTo(_scrollController.offset / 120); // if scroll for above 150, then change app bar color to white, search button to dark, and top icon color to dark // if scroll for below 150, then change app bar color to transparent, search button to white and top icon color to light if (_scrollController.hasClients && _scrollController.offset > (150 - kToolbarHeight)) { if (_topIconColor != BLACK_GREY) { _topIconColor = BLACK_GREY; _topSearchColor = Colors.grey[100]!; _appBarSystemOverlayStyle = SystemUiOverlayStyle.dark; } } else { if (_topIconColor != Colors.white) { _topIconColor = Colors.white; _topSearchColor = Colors.white; _appBarSystemOverlayStyle = SystemUiOverlayStyle.light; } } }); } @override Widget build(BuildContext context) { // if we used AutomaticKeepAliveClientMixin, we must call super.build(context); super.build(context); final double boxImageSize = (MediaQuery.of(context).size.width / 3); final double categoryForYouHeightShort = boxImageSize; final double categoryForYouHeightLong = (boxImageSize * 2); return Scaffold( body: Stack( children: [ CustomScrollView( controller: _scrollController, slivers: [ SliverList( delegate: SliverChildListDelegate([ _createHomeBannerSlider(), _createCoupon(), _createGridCategory(), Container( margin: EdgeInsets.only(top: 10, left: 16, right: 16), child: Text('Flash Sale', style: GlobalStyle.sectionTitle), ), Container( margin: EdgeInsets.only(top: 4, left: 16, right: 16), child: Row( children: [ Text('Flash sale end in ', style: TextStyle( fontWeight: FontWeight.normal, fontSize: 13, color: CHARCOAL)), _buildFlashsaleTime(), Expanded( child: GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => FlashSalePage( seconds: _flashsaleSecond))); }, child: Text('View All', style: GlobalStyle.viewAll, textAlign: TextAlign.end), ), ) ], ), ), Container( margin: EdgeInsets.only(top: 16), height: boxImageSize * GlobalStyle.horizontalProductHeightMultiplication, child: ListView.builder( padding: EdgeInsets.only(left: 12, right: 12), scrollDirection: Axis.horizontal, itemCount: flashsaleData.length, itemBuilder: (BuildContext context, int index) { return _buildFlashsaleCard(index, boxImageSize); }, ), ), Container( margin: EdgeInsets.only(top: 30, left: 16, right: 16), child: Text('Trending Product', style: GlobalStyle.sectionTitle), ), Container( margin: EdgeInsets.fromLTRB(12, 0, 12, 0), child: GridView.count( padding: EdgeInsets.fromLTRB(0, 8, 0, 0), primary: false, childAspectRatio: 4 / 1.6, shrinkWrap: true, crossAxisSpacing: 2, mainAxisSpacing: 2, crossAxisCount: 2, children: List.generate(homeTrendingData.length, (index) { return _buildTrendingProductCard(index); }), ), ), Container( margin: EdgeInsets.only(top: 30, left: 16, right: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Last Search', style: GlobalStyle.sectionTitle), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => LastSearchPage())); }, child: Text('View All', style: GlobalStyle.viewAll, textAlign: TextAlign.end), ) ], ), ), Container( margin: EdgeInsets.only(top: 16), height: boxImageSize * GlobalStyle.horizontalProductHeightMultiplication, child: ListView.builder( padding: EdgeInsets.only(left: 12, right: 12), scrollDirection: Axis.horizontal, itemCount: lastSearchData.length, itemBuilder: (BuildContext context, int index) { return _buildLastSearchCard(index, boxImageSize); }, ), ), Container( margin: EdgeInsets.only(top: 30, left: 16, right: 16), child: Text('Category For You', style: GlobalStyle.sectionTitle), ), _createCategoryForYou(boxImageSize, categoryForYouHeightShort, categoryForYouHeightLong), Container( margin: EdgeInsets.only(top: 30, left: 16, right: 16), child: Text('Recomended Product', style: GlobalStyle.sectionTitle), ), CustomScrollView( shrinkWrap: true, primary: false, slivers: [ SliverPadding( padding: EdgeInsets.fromLTRB(12, 8, 12, 8), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: GlobalStyle.gridDelegateRatio, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return _buildRecomendedProductCard(index); }, childCount: recomendedProductData.length, ), ), ), ]), ])), ], ), // Create AppBar with Animation Container( height: AppBar().preferredSize.height + MediaQuery.of(context).padding.top - 20 + 22, child: AnimatedBuilder( animation: _topColorAnimationController, builder: (context, child) => AppBar( automaticallyImplyLeading: false, backgroundColor: _appBarColor.value, systemOverlayStyle: _appBarSystemOverlayStyle, elevation: GlobalStyle.appBarElevation, title: Container( child: TextButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith( (Set states) => _topSearchColor, ), overlayColor: MaterialStateProperty.all(Colors.transparent), shape: MaterialStateProperty.all(RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), )), ), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => SearchPage())); }, child: Row( children: [ SizedBox(width: 8), Icon( Icons.search, color: Colors.grey[500], size: 18, ), SizedBox(width: 8), Text( 'Search Product', style: TextStyle( color: Colors.grey[500], fontWeight: FontWeight.normal), ) ], )), ), actions: [ GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ChatUsPage())); }, child: Icon(Icons.email, color: _topIconColor)), IconButton( icon: _reusableWidget.customNotifIcon( count: 8, notifColor: _topIconColor), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => NotificationPage())); }), ], ), ), ) ], ), ); } Widget _createHomeBannerSlider() { return Column( children: [ CarouselSlider( items: homeBannerData .map((item) => Container( child: buildCacheNetworkImage( width: 0, height: 0, url: item.image), )) .toList(), options: CarouselOptions( aspectRatio: 8 / 6, viewportFraction: 1.0, autoPlay: true, autoPlayInterval: Duration(seconds: 6), autoPlayAnimationDuration: Duration(milliseconds: 300), enlargeCenterPage: false, onPageChanged: (index, reason) { setState(() { _currentImageSlider = index; }); }), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: homeBannerData.map((item) { int index = homeBannerData.indexOf(item); return Container( width: 8.0, height: 8.0, margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0), decoration: BoxDecoration( shape: BoxShape.circle, color: _currentImageSlider == index ? PRIMARY_COLOR : Colors.grey[300], ), ); }).toList(), ), ], ); } Widget _createCoupon() { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => CouponPage())); }, child: Container( padding: EdgeInsets.all(12), margin: EdgeInsets.all(16.0), decoration: BoxDecoration( color: SOFT_BLUE, borderRadius: BorderRadius.circular(5)), child: Row( children: [ Expanded( child: Container( child: Text( 'There are 10 coupon waiting', style: TextStyle( fontSize: 14, color: Color(0xffffffff), fontWeight: FontWeight.bold), ), ), ), Icon(Icons.local_offer, color: Colors.white) ], ), ), ); } Widget _buildFlashsaleTime() { int hour = _flashsaleSecond ~/ 3600; int minute = _flashsaleSecond % 3600 ~/ 60; int second = _flashsaleSecond % 60; return Row( children: [ Container( padding: EdgeInsets.fromLTRB(3, 4, 3, 4), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(5)), // child: Text(_globalFunction.formatTime(hour), style: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)), ), Text(' : ', style: TextStyle( color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold)), Container( padding: EdgeInsets.fromLTRB(3, 4, 3, 4), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(5)), // child: Text(_globalFunction.formatTime(minute), style: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)), ), Text(' : ', style: TextStyle( color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold)), Container( padding: EdgeInsets.fromLTRB(3, 4, 3, 4), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(5)), // child: Text(_globalFunction.formatTime(second), style: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)), ) ], ); } Widget _createGridCategory() { return GridView.count( padding: EdgeInsets.fromLTRB(16, 16, 16, 0), primary: false, childAspectRatio: 1.1, shrinkWrap: true, crossAxisSpacing: 0, mainAxisSpacing: 0, crossAxisCount: 4, children: List.generate(categoryData.length, (index) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryData[index].id, categoryName: categoryData[index].name))); }, child: Column(children: [ buildCacheNetworkImage( width: 40, height: 40, url: categoryData[index].image, plColor: Colors.transparent), Flexible( child: Container( margin: EdgeInsets.fromLTRB(0, 10, 0, 0), child: Text( categoryData[index].name, style: TextStyle( color: CHARCOAL, fontWeight: FontWeight.normal, fontSize: 12, ), textAlign: TextAlign.center, ), ), ) ])); }), ); } Widget _buildFlashsaleCard(index, boxImageSize) { return Container( width: boxImageSize + 10, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 2, color: Colors.white, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductDetailPage( name: flashsaleData[index].name, image: flashsaleData[index].image, price: flashsaleData[index].price, rating: 4, review: 45, sale: flashsaleData[index].sale))); }, child: Column( children: [ Stack( children: [ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10)), child: buildCacheNetworkImage( width: boxImageSize + 10, height: boxImageSize + 10, url: flashsaleData[index].image)), Positioned( right: 0, top: 10, child: Container( decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.only( topLeft: Radius.circular(6), bottomLeft: Radius.circular(6))), padding: EdgeInsets.fromLTRB(8, 4, 8, 4), child: Text( flashsaleData[index].discount.toString() + '%', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)), ), ) ], ), Container( margin: EdgeInsets.fromLTRB(8, 8, 8, 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( flashsaleData[index].name, style: GlobalStyle.productName, maxLines: 2, overflow: TextOverflow.ellipsis, ), Container( margin: EdgeInsets.only(top: 5), child: Text( '\$ ' + _globalFunction.removeDecimalZeroFormat( flashsaleData[index].price), style: GlobalStyle.productPriceDiscounted), ), Container( margin: EdgeInsets.only(top: 2), child: Text( '\$ ' + _globalFunction.removeDecimalZeroFormat( ((100 - flashsaleData[index].discount) * flashsaleData[index].price / 100)), style: GlobalStyle.productPrice), ) ], ), ) ], ), ), ), ); } Widget _buildTrendingProductCard(index) { return GestureDetector( onTap: () { StatefulWidget menuPage = SearchProductPage(words: homeTrendingData[index].name); Navigator.push( context, MaterialPageRoute(builder: (context) => menuPage)); }, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 2, color: Colors.white, child: Row( children: [ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), child: buildCacheNetworkImage( width: (MediaQuery.of(context).size.width / 2) * (1.6 / 4) - 12 - 1, height: (MediaQuery.of(context).size.width / 2) * (1.6 / 4) - 12 - 1, url: homeTrendingData[index].image)), Expanded( child: Container( margin: EdgeInsets.all(10), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(homeTrendingData[index].name, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold)), SizedBox(height: 4), Text(homeTrendingData[index].sale + ' Product', style: TextStyle(fontSize: 9, color: BLACK_GREY)) ], ), ), ) ], )), ); } Widget _buildLastSearchCard(index, boxImageSize) { return Container( width: boxImageSize + 10, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 2, color: Colors.white, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductDetailPage( name: lastSearchData[index].name, image: lastSearchData[index].image, price: lastSearchData[index].price, rating: lastSearchData[index].rating, review: lastSearchData[index].review, sale: lastSearchData[index].sale))); }, child: Column( children: [ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10)), child: buildCacheNetworkImage( width: boxImageSize + 10, height: boxImageSize + 10, url: lastSearchData[index].image)), Container( margin: EdgeInsets.fromLTRB(8, 8, 8, 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( lastSearchData[index].name, style: GlobalStyle.productName, maxLines: 2, overflow: TextOverflow.ellipsis, ), Container( margin: EdgeInsets.only(top: 5), child: Text( '\$ ' + _globalFunction.removeDecimalZeroFormat( lastSearchData[index].price), style: GlobalStyle.productPrice), ), Container( margin: EdgeInsets.only(top: 5), child: Row( children: [ _reusableWidget.createRatingBar( rating: lastSearchData[index].rating, size: 12), Text( '(' + lastSearchData[index].review.toString() + ')', style: GlobalStyle.productTotalReview) ], ), ) ], ), ), ], ), ), ), ); } Widget _createCategoryForYou( boxImageSize, categoryForYouHeightShort, categoryForYouHeightLong) { return Container( margin: EdgeInsets.only(top: 8), width: MediaQuery.of(context).size.width, height: categoryForYouHeightLong, child: Row( children: [ GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryForYouData[0].id, categoryName: categoryData[0].name))); }, child: Container( width: boxImageSize, height: categoryForYouHeightLong, child: buildCacheNetworkImage( width: 0, height: 0, url: categoryForYouData[0].image), ), ), Column( children: [ Row( children: [ GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryForYouData[1].id, categoryName: categoryData[1].name))); }, child: Container( width: boxImageSize, height: categoryForYouHeightShort, child: buildCacheNetworkImage( width: 0, height: 0, url: categoryForYouData[1].image), ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryForYouData[2].id, categoryName: categoryData[2].name))); }, child: Container( width: boxImageSize, height: categoryForYouHeightShort, child: buildCacheNetworkImage( width: 0, height: 0, url: categoryForYouData[2].image), ), ) ], ), Row( children: [ GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryForYouData[3].id, categoryName: categoryData[3].name))); }, child: Container( width: boxImageSize, height: categoryForYouHeightShort, child: buildCacheNetworkImage( width: 0, height: 0, url: categoryForYouData[3].image), ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductCategoryPage( categoryId: categoryForYouData[4].id, categoryName: categoryData[4].name))); }, child: Container( width: boxImageSize, height: categoryForYouHeightShort, child: buildCacheNetworkImage( width: 0, height: 0, url: categoryForYouData[4].image), ), ) ], ) ], ) ], ), ); } Widget _buildRecomendedProductCard(index) { final double boxImageSize = ((MediaQuery.of(context).size.width) - 24) / 2 - 12; return Container( child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 2, color: Colors.white, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductDetailPage( name: recomendedProductData[index].name, image: recomendedProductData[index].image, price: recomendedProductData[index].price, rating: recomendedProductData[index].rating, review: recomendedProductData[index].review, sale: recomendedProductData[index].sale))); }, child: Column( children: [ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10)), child: buildCacheNetworkImage( width: boxImageSize, height: boxImageSize, url: recomendedProductData[index].image)), Container( margin: EdgeInsets.fromLTRB(8, 8, 8, 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recomendedProductData[index].name, style: GlobalStyle.productName, maxLines: 2, overflow: TextOverflow.ellipsis, ), Container( margin: EdgeInsets.only(top: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '\$ ' + _globalFunction.removeDecimalZeroFormat( recomendedProductData[index].price), style: GlobalStyle.productPrice), Text( recomendedProductData[index].sale.toString() + ' Sale', style: TextStyle(fontSize: 11, color: SOFT_GREY)) ], ), ), Container( margin: EdgeInsets.only(top: 5), child: Row( children: [ Icon(Icons.location_on, color: SOFT_GREY, size: 12), Text(' ' + recomendedProductData[index].location, style: GlobalStyle.productSale) ], ), ), Container( margin: EdgeInsets.only(top: 5), child: Row( children: [ _reusableWidget.createRatingBar( rating: recomendedProductData[index].rating, size: 12), Text( '(' + recomendedProductData[index] .review .toString() + ')', style: GlobalStyle.productTotalReview) ], ), ) ], ), ), ], ), ), ), ); } }