Created with <3 with dartpad.dev.
Last active
May 19, 2023 21:31
-
-
Save plotsklapps/286534abf10a66995bff9ff08b13e77d to your computer and use it in GitHub Desktop.
Flutter Architecture #3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:provider/provider.dart'; | |
// MAIN | |
void main() { | |
runApp( | |
MultiProvider( | |
providers: [ | |
ChangeNotifierProvider(create: (_) { | |
return DonutBottomBarSelectionService(); | |
}), | |
ChangeNotifierProvider(create: (_) { | |
return DonutService(); | |
}), | |
], | |
child: MaterialApp( | |
debugShowCheckedModeBanner: false, | |
initialRoute: '/', | |
navigatorKey: Utils.mainAppNav, | |
routes: { | |
'/': (BuildContext context) { | |
return const SplashPage(); | |
}, | |
'/main': (BuildContext context) { | |
return const DonutShopMain(); | |
}, | |
'/details': (BuildContext context) { | |
return const DonutShopDetails(); | |
} | |
}, | |
), | |
), | |
); | |
} | |
// PAGES | |
class SplashPage extends StatefulWidget { | |
const SplashPage({super.key}); | |
@override | |
SplashPageState createState() { | |
return SplashPageState(); | |
} | |
} | |
class SplashPageState extends State<SplashPage> | |
with SingleTickerProviderStateMixin { | |
// The SingleTickerProviderStateMixin mixin will provide this class | |
// with the Ticker instance it needs; an AnimationController needs a | |
// TickerProvider — the AnimationController constructor takes a required | |
// parameter vsync that must implement a TickerProvider interface, | |
// therefore we are implementing SingleTickerProviderStateMixin on this | |
// class so it serves as the AnimationController's ticker provider. | |
AnimationController? donutController; | |
Animation<double>? rotationAnimation; | |
@override | |
void initState() { | |
super.initState(); | |
// vsync: the instance of the Ticker provider; in this case, the | |
// existing instance (this) since we are implementing the | |
// SingleTickerProviderStateMixin. Follow the instantiation by quickly | |
// executing its repeat method by using the spread operator | |
// (the two dots right after the instance; i.e. ..repeat()). | |
// What this does is allow the animation to repeat in an infinite loop. | |
donutController = AnimationController( | |
duration: const Duration(seconds: 5), | |
vsync: this, | |
)..repeat(); | |
rotationAnimation = Tween<double>( | |
begin: 0, | |
end: 1, | |
).animate( | |
CurvedAnimation( | |
parent: donutController!, | |
curve: Curves.linear, | |
), | |
); | |
} | |
@override | |
void dispose() { | |
donutController!.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// Future.delayed takes two parameters: a Duration object | |
// with it's seconds property set to 2, and a callback. | |
// When the 2 seconds have ellapsed, it will call the callback. | |
Future.delayed(const Duration(seconds: 2), () { | |
Utils.mainAppNav.currentState!.pushReplacementNamed( | |
'/main', | |
); | |
}); | |
return Scaffold( | |
backgroundColor: Utils.mainColor, | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
RotationTransition( | |
turns: rotationAnimation!, | |
child: Image.network( | |
Utils.donutLogoWhiteNoText, | |
width: 100.0, | |
height: 100.0, | |
), | |
), | |
Image.network( | |
Utils.donutLogoWhiteText, | |
width: 150.0, | |
height: 150.0, | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class DonutShopMain extends StatelessWidget { | |
const DonutShopMain({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
iconTheme: const IconThemeData( | |
color: Utils.mainDark, | |
), | |
backgroundColor: Colors.transparent, | |
elevation: 0.0, | |
centerTitle: true, | |
title: Image.network( | |
Utils.donutLogoRedText, | |
), | |
), | |
drawer: const Drawer( | |
child: DonutSideMenu(), | |
), | |
body: Column( | |
children: [ | |
Expanded( | |
child: Navigator( | |
key: Utils.mainListNav, | |
initialRoute: '/main', | |
// When a user calls Utils.mainListNav.currentState.pushNamed() | |
// and pass the named route belonging to this child navigation stack, | |
// the onGenerateRoute gets invoked. Its settings parameter type | |
// RouteSettings has a property called named which you should use | |
// to match the provided named route to the widget to be pushed at | |
// the top of this child navigation stack. | |
// At the end of the onGenerateRoute method, return a | |
// PageRouteBuilder, provided a pageBuilder callback, which | |
// returns the widget to be displayed onto the stack, as well | |
// as a transitionDuration, in case you want to provide a duration | |
// - in our case, we set a Duration to zero so no transition occurs. | |
onGenerateRoute: (RouteSettings settings) { | |
Widget page; | |
if (settings.name == '/favorites') { | |
page = const Center( | |
child: Text('favorites'), | |
); | |
} else if (settings.name == '/shoppingcart') { | |
page = const Center( | |
child: Text('shopping cart'), | |
); | |
} else { | |
page = const DonutMainPage(); | |
} | |
return PageRouteBuilder( | |
pageBuilder: ( | |
_, | |
__, | |
___, | |
) => | |
page, | |
transitionDuration: const Duration(seconds: 0), | |
); | |
}), | |
), | |
const DonutBottomBar(), | |
], | |
), | |
); | |
} | |
} | |
class DonutMainPage extends StatelessWidget { | |
const DonutMainPage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: [ | |
const DonutPager(), | |
const DonutFilterBar(), | |
Expanded(child: Consumer<DonutService>(builder: ( | |
BuildContext context, | |
DonutService donutService, | |
Widget? child, | |
) { | |
return DonutList(donutsList: donutService.filteredDonuts); | |
})), | |
], | |
); | |
} | |
} | |
// WIDGETS | |
class DonutSideMenu extends StatelessWidget { | |
const DonutSideMenu({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: Utils.mainDark, | |
padding: const EdgeInsets.all(40.0), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Container( | |
margin: const EdgeInsets.only(top: 40.0), | |
child: Image.network( | |
Utils.donutLogoWhiteNoText, | |
width: 100.0, | |
), | |
), | |
Image.network( | |
Utils.donutLogoWhiteText, | |
width: 150.0, | |
), | |
], | |
), | |
); | |
} | |
} | |
class DonutPager extends StatefulWidget { | |
const DonutPager({super.key}); | |
@override | |
State<DonutPager> createState() { | |
return DonutPagerState(); | |
} | |
} | |
class DonutPagerState extends State<DonutPager> { | |
List<DonutPage> pages = [ | |
DonutPage( | |
imgUrl: Utils.donutPromo1, | |
logoImgUrl: Utils.donutLogoWhiteText, | |
), | |
DonutPage( | |
imgUrl: Utils.donutPromo2, | |
logoImgUrl: Utils.donutLogoWhiteText, | |
), | |
DonutPage( | |
imgUrl: Utils.donutPromo3, | |
logoImgUrl: Utils.donutLogoRedText, | |
), | |
]; | |
int currentPage = 0; | |
PageController? pageController; | |
@override | |
void initState() { | |
super.initState(); | |
pageController = PageController(initialPage: 0); | |
} | |
@override | |
void dispose() { | |
pageController!.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return SizedBox( | |
height: 350.0, | |
child: Column( | |
children: [ | |
Expanded( | |
child: PageView( | |
scrollDirection: Axis.horizontal, | |
pageSnapping: true, | |
controller: pageController, | |
onPageChanged: (int page) { | |
setState(() { | |
currentPage = page; | |
}); | |
}, | |
// List.generate's callback method receives an index, which represents | |
// the current item in the iteration, with which we'll pull the | |
// corresponding DonutPage object from the list of pages to build the page. | |
children: List.generate(pages.length, (index) { | |
DonutPage currentPage = pages[index]; | |
return Container( | |
alignment: Alignment.bottomLeft, | |
margin: const EdgeInsets.all(20.0), | |
padding: const EdgeInsets.all(30.0), | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(30.0), | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.black.withOpacity(0.2), | |
blurRadius: 10.0, | |
offset: const Offset(0.0, 5.0), | |
), | |
], | |
image: DecorationImage( | |
image: NetworkImage(currentPage.imgUrl!), | |
fit: BoxFit.cover, | |
), | |
), | |
child: Image.network( | |
currentPage.logoImgUrl!, | |
width: 120.0, | |
), | |
); | |
}), | |
), | |
), | |
PageViewIndicator( | |
pageController: pageController, | |
numberOfPages: pages.length, | |
currentPage: currentPage, | |
), | |
], | |
), | |
); | |
} | |
} | |
class PageViewIndicator extends StatelessWidget { | |
final PageController? pageController; | |
final int? numberOfPages; | |
final int? currentPage; | |
const PageViewIndicator({super.key, | |
this.pageController, | |
this.numberOfPages, | |
this.currentPage, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: List.generate(numberOfPages!, (index) { | |
return GestureDetector( | |
onTap: () { | |
// We will leverage the GestureDetector's onTap event, | |
// and inside it, use the PageController's animateToPage method, | |
// to which you'll pass the index of the page indicator (which | |
// will be the same as the page index), the duration of the | |
// page sliding effect, and the curve of the animation. | |
pageController!.animateToPage( | |
index, | |
duration: const Duration(milliseconds: 500), | |
curve: Curves.easeInOut, | |
); | |
}, | |
child: AnimatedContainer( | |
duration: const Duration(milliseconds: 250), | |
curve: Curves.easeInOut, | |
width: 15.0, | |
height: 15.0, | |
margin: const EdgeInsets.all(10.0), | |
decoration: BoxDecoration( | |
color: currentPage == index | |
? Utils.mainColor | |
: Colors.grey.withOpacity(0.2), | |
borderRadius: BorderRadius.circular(10.0), | |
), | |
), | |
); | |
}), | |
); | |
} | |
} | |
class DonutList extends StatefulWidget { | |
final List<DonutModel>? donutsList; | |
const DonutList({super.key, this.donutsList}); | |
@override | |
State<DonutList> createState() { | |
return DonutListState(); | |
} | |
} | |
class DonutListState extends State<DonutList> { | |
// Add a GlobalKey reference in the _DonutListState class. | |
// All AnimatedList widgets require a key in order to tap into | |
// its inserting capabilities so we can uniquely reference them | |
// outside of the build method of the enclosing widget. | |
final GlobalKey<AnimatedListState> key = GlobalKey(); | |
List<DonutModel> insertedItems = []; | |
@override | |
void initState() { | |
super.initState(); | |
// Each item will be inserted from the main collection (widget.donutsList) | |
// to a holding collection (insertedItems). | |
// As they are inserted, we introduce a delay of 125ms. | |
// The AnimatedList widget receives one at a time within 125 ms from | |
// each other, and performs whatever animation you decide. | |
var future = Future(() {}); | |
for (var i = 0; i < widget.donutsList!.length; i++) { | |
future = future.then((_) { | |
return Future.delayed(const Duration(milliseconds: 125), () { | |
insertedItems.add(widget.donutsList![i]); | |
key.currentState!.insertItem(i); | |
}); | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return AnimatedList( | |
key: key, | |
scrollDirection: Axis.horizontal, | |
initialItemCount: insertedItems.length, | |
itemBuilder: ( | |
BuildContext context, | |
int index, | |
animation, | |
) { | |
DonutModel currentDonut = widget.donutsList![index]; | |
return SlideTransition( | |
position: Tween( | |
begin: const Offset(0.2, 0.0), | |
end: const Offset(0.0, 0.0), | |
).animate( | |
CurvedAnimation( | |
parent: animation, | |
curve: Curves.easeInOut, | |
), | |
), | |
child: FadeTransition( | |
opacity: Tween(begin: 0.0, end: 1.0).animate( | |
CurvedAnimation( | |
parent: animation, | |
curve: Curves.easeInOut, | |
), | |
), | |
child: DonutCard(donutInfo: currentDonut), | |
), | |
); | |
}); | |
} | |
} | |
class DonutCard extends StatelessWidget { | |
// Add a new property to this DonutCard widget called donutInfo, | |
// type DonutModel, and feed it via the constructor. We'll use | |
// this property to feed data into this widget and render the | |
// text labels and image | |
final DonutModel? donutInfo; | |
const DonutCard({super.key, this.donutInfo}); | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () { | |
// Obtain reference to the DonutService using the Provider.of | |
// instance along with the provided Context, using the | |
// listen: false option. | |
var donutService = Provider.of<DonutService>( | |
context, | |
listen: false, | |
); | |
// With the DonutService in hand, call the method onDonutSelected and | |
// supply the reference to a DonutModel available in this widget | |
// called donutInfo. | |
donutService.onDonutSelected(donutInfo!); | |
}, | |
child: Stack( | |
alignment: Alignment.center, | |
children: [ | |
Container( | |
width: 150.0, | |
padding: const EdgeInsets.all(15.0), | |
margin: const EdgeInsets.fromLTRB(10.0, 80.0, 0.0, 20.0), | |
alignment: Alignment.bottomLeft, | |
decoration: BoxDecoration( | |
color: Colors.white, | |
borderRadius: BorderRadius.circular(20.0), | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.black.withOpacity(0.05), | |
blurRadius: 10.0, | |
offset: const Offset(0.0, 4.0), | |
), | |
], | |
), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.end, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
donutInfo!.name!, | |
style: const TextStyle( | |
color: Utils.mainDark, | |
fontWeight: FontWeight.bold, | |
fontSize: 15.0, | |
), | |
), | |
const SizedBox(height: 10.0), | |
Container( | |
padding: const EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0), | |
decoration: BoxDecoration( | |
color: Utils.mainColor, | |
borderRadius: BorderRadius.circular(20.0), | |
), | |
child: Center( | |
child: Text( | |
'\$${donutInfo!.price!.toStringAsFixed(2)}', | |
style: const TextStyle( | |
fontSize: 12.0, | |
color: Colors.white, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: Hero( | |
// Both source hero tag and destination hero tag as | |
// well as the images must match for the animation | |
// work effectively. | |
tag: donutInfo!.name!, | |
child: Image.network( | |
donutInfo!.imgUrl!, | |
width: 150.0, | |
height: 150.0, | |
fit: BoxFit.contain, | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class DonutFilterBar extends StatelessWidget { | |
const DonutFilterBar({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
Alignment alignmentBasedOnTap(filterBarId) { | |
if (filterBarId == 'sprinkled') { | |
return Alignment.center; | |
} else if (filterBarId == 'stuffed') { | |
return Alignment.centerRight; | |
} else { | |
return Alignment.centerLeft; | |
} | |
} | |
return Padding( | |
padding: const EdgeInsets.all(20.0), | |
child: Consumer<DonutService>(builder: ( | |
BuildContext context, | |
donutService, | |
Widget? child, | |
) { | |
return Column( | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: | |
List.generate(donutService.filterBarItems.length, (index) { | |
DonutFilterBarItem item = donutService.filterBarItems[index]; | |
return GestureDetector( | |
onTap: () { | |
// Invoke the donutService.filteredDonutsByType method, | |
// passing the corresponding item's id upon the user | |
// tapping on the filter bar item. | |
donutService.filteredDonutsByType(item.id!); | |
}, | |
child: Text( | |
item.label!, | |
style: TextStyle( | |
color: donutService.selectedDonutType == item.id | |
? Utils.mainColor | |
: Colors.black, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
); | |
}), | |
), | |
const SizedBox(height: 10.0), | |
Stack( | |
children: [ | |
AnimatedAlign( | |
duration: const Duration(milliseconds: 250), | |
curve: Curves.easeInOut, | |
alignment: | |
alignmentBasedOnTap(donutService.selectedDonutType), | |
child: Container( | |
width: MediaQuery.of(context).size.width / 3 - 20.0, | |
height: 5.0, | |
decoration: BoxDecoration( | |
color: Utils.mainColor, | |
borderRadius: BorderRadius.circular(20.0), | |
), | |
), | |
), | |
], | |
), | |
], | |
); | |
}), | |
); | |
} | |
} | |
class DonutBottomBar extends StatelessWidget { | |
const DonutBottomBar({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
padding: const EdgeInsets.all(30.0), | |
// Now that the service has been provided at the root, since the | |
// DonutBottomBar widget is a widget located down the hierarchy, | |
// we know for sure this service will trickle down to it. In order | |
// to consume it - listen to changes occurring when the tab selection | |
// changes (i.e. the setTabSelection method gets called), we will use | |
// a convenient widget called Consumer. The Consumer widget rebuilds | |
// itself when the notifyListeners method of the service it is | |
// listening to gets called. | |
// Notice how we make the Consumer widget a direct child of the main | |
// Container widget in the bottom bar, which in turn returns the Row | |
// widget from its builder callback method. This callback provides | |
// a context, an instance of the service being listened to | |
// (bottomBarSelectionService) and an optional child. | |
child: Consumer<DonutBottomBarSelectionService>(builder: ( | |
BuildContext context, | |
DonutBottomBarSelectionService bottomBarSelectionService, | |
Widget? child, | |
) { | |
return Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
crossAxisAlignment: CrossAxisAlignment.end, | |
children: [ | |
// Each button will call up to the service, set the value, | |
// and thus invoking the notifyListeners method, which will | |
// cause the Consumer widget to rebuild. | |
IconButton( | |
onPressed: () { | |
bottomBarSelectionService.setTabSelection( | |
'main', | |
); | |
}, | |
icon: Icon( | |
Icons.trip_origin, | |
color: bottomBarSelectionService.tabSelection == 'main' | |
? Utils.mainDark | |
: Utils.mainColor, | |
), | |
), | |
IconButton( | |
onPressed: () { | |
bottomBarSelectionService.setTabSelection( | |
'favorites', | |
); | |
}, | |
icon: Icon( | |
Icons.favorite, | |
color: bottomBarSelectionService.tabSelection == 'favorites' | |
? Utils.mainDark | |
: Utils.mainColor, | |
), | |
), | |
IconButton( | |
onPressed: () { | |
bottomBarSelectionService.setTabSelection( | |
'shoppingcart', | |
); | |
}, | |
icon: Icon( | |
Icons.shopping_cart, | |
color: | |
bottomBarSelectionService.tabSelection == 'shoppingcart' | |
? Utils.mainDark | |
: Utils.mainColor, | |
), | |
), | |
]); | |
}), | |
); | |
} | |
} | |
class DonutShopDetails extends StatefulWidget { | |
const DonutShopDetails({super.key}); | |
@override | |
State<DonutShopDetails> createState() { | |
return DonutShopDetailsState(); | |
} | |
} | |
class DonutShopDetailsState extends State<DonutShopDetails> | |
with SingleTickerProviderStateMixin { | |
DonutModel? selectedDonut; | |
AnimationController? controller; | |
Animation<double>? rotationAnimation; | |
@override | |
void initState() { | |
super.initState(); | |
// For the AnimationController, we set its duration to 20 secs, | |
// and assigned the vsync property to this - this means since we've | |
// made the current widget a SingleTickerProvider, we pass ourselves in. | |
// Calling the repeat method on the controller means this animation will | |
// repeat forever, infinitely - until this widget gets destroyed. | |
controller = | |
AnimationController(duration: const Duration(seconds: 20), vsync: this) | |
..repeat(); | |
// For the Animation (rotationAnimation) we use a Tween object, | |
// pass a begin value of 0 and an end of 1 - this means make the animation | |
// completed its whole cycle, and since the AnimationController is in repeat, | |
// after it concludes, it starts again. Therefore we invoke the Tween | |
// object's animate method, where pass the controller as the parent, as | |
// well as the animation's curve (in our case, we want it linear, | |
// hence the Curves.linear option). | |
rotationAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( | |
CurvedAnimation( | |
parent: controller!, | |
curve: Curves.linear, | |
), | |
); | |
} | |
@override | |
void dispose() { | |
controller!.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// Pull a reference to the DonutService using the Provider.of | |
// method with the listen: false option. | |
var donutService = Provider.of<DonutService>( | |
context, | |
listen: false, | |
); | |
// Extract the selected donut by calling the method getSelectedDonut | |
// out of the DonutService reference, and assigning it to the local | |
// property selectedDonut. | |
selectedDonut = donutService.getSelectedDonut(); | |
return Scaffold( | |
appBar: AppBar( | |
iconTheme: const IconThemeData(color: Utils.mainDark), | |
backgroundColor: Colors.transparent, | |
elevation: 0.0, | |
centerTitle: true, | |
title: SizedBox( | |
width: 120.0, | |
child: Image.network(Utils.donutLogoRedText), | |
), | |
), | |
body: Column( | |
children: [ | |
SizedBox( | |
height: MediaQuery.of(context).size.height / 2, | |
child: Stack( | |
children: [ | |
// A Positioned widget must be a direct descendant of the Stack. | |
// The Positioned widget positions the wrapping widget in regards | |
// to the parent Stack coordinates (0,0 being the very top left | |
// corner of the Stack and so on). | |
Positioned( | |
top: -40.0, | |
right: -120.0, | |
child: Hero( | |
tag: selectedDonut!.name!, | |
child: RotationTransition( | |
turns: rotationAnimation!, | |
child: Image.network( | |
selectedDonut!.imgUrl!, | |
width: MediaQuery.of(context).size.width * 1.25, | |
fit: BoxFit.contain, | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
Expanded( | |
child: Padding( | |
padding: const EdgeInsets.all(30.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Row( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Expanded( | |
child: Text( | |
selectedDonut!.name!, | |
style: const TextStyle( | |
color: Utils.mainDark, | |
fontSize: 30.0, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
const SizedBox(width: 50.0), | |
IconButton( | |
onPressed: () {}, | |
color: Utils.mainDark, | |
icon: const Icon( | |
Icons.favorite_outline, | |
), | |
), | |
], | |
), | |
const SizedBox(height: 10.0), | |
Container( | |
padding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), | |
decoration: BoxDecoration( | |
color: Utils.mainDark, | |
borderRadius: BorderRadius.circular(20.0), | |
), | |
child: Text( | |
'\$${selectedDonut!.price!.toStringAsFixed(2)}', | |
style: const TextStyle( | |
color: Colors.white, | |
), | |
), | |
), | |
const SizedBox(height: 20.0), | |
Text(selectedDonut!.description!), | |
Container( | |
padding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), | |
margin: const EdgeInsets.only(top: 20.0), | |
alignment: Alignment.center, | |
decoration: BoxDecoration( | |
color: Utils.mainColor.withOpacity(0.1), | |
borderRadius: BorderRadius.circular(50.0), | |
), | |
child: const Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Icon( | |
Icons.shopping_cart, | |
color: Utils.mainDark, | |
), | |
SizedBox(width: 20.0), | |
Text( | |
'Add to cart', | |
style: TextStyle( | |
color: Utils.mainDark, | |
), | |
), | |
], | |
), | |
), | |
], | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
// SERVICES | |
// The ChangeNotifier class provides change notification capabilities | |
// for those interested widgets in knowing when changes occur in any of | |
// their properties, which is accomplished by calling its notifyListeners | |
// method. | |
class DonutBottomBarSelectionService extends ChangeNotifier { | |
// We are interested in storing what the current bottom bar item | |
// selection is at any given time, so we'll store that in a variable | |
// called tabSelection, type string. We'll have a default value of "main" | |
// which means there will be a bottom bar item named "main" that will | |
// always be selected by default. | |
String? tabSelection = 'main'; | |
// We will provide a handy setter method called setTabSelection that | |
// takes the new selection, and right after storing the new value, we | |
// will invoke notifyListeners, which notifies any listening widgets | |
// (i.e. Consumer widget) to rebuild based on the new changes. | |
void setTabSelection(String selection) { | |
Utils.mainListNav.currentState!.pushReplacementNamed('/$selection'); | |
tabSelection = selection; | |
notifyListeners(); | |
} | |
} | |
class DonutService extends ChangeNotifier { | |
List<DonutFilterBarItem> filterBarItems = [ | |
DonutFilterBarItem( | |
id: 'classic', | |
label: 'Classic', | |
), | |
DonutFilterBarItem( | |
id: 'sprinkled', | |
label: 'Sprinkled', | |
), | |
DonutFilterBarItem( | |
id: 'stuffed', | |
label: 'Stuffed', | |
), | |
]; | |
String? selectedDonutType; | |
List<DonutModel> filteredDonuts = []; | |
// The late modifier marks this property for delayed initialization, | |
// since by making it nullable (using the ‘?') we are implying that | |
// at some point this property may be null - a useful, meaningful | |
// value for this field, which is not the case. | |
late DonutModel selectedDonut; | |
// Create a method called getSelectedDonut() which will serve as | |
// a getter method or wrapper to the selectedDonut property. | |
DonutModel getSelectedDonut() { | |
return selectedDonut; | |
} | |
// Create a method called onDonutSelected, that takes as a parameter | |
// an object of type DonutModel. The key here is the following: we'll | |
// encapsulate even the navigation to the details page once we've | |
// ensured the selectedDonut property has been set, which guarantees | |
// that when we've navigated to the details page (DonutShopDetails) | |
// the value will be available. | |
void onDonutSelected(DonutModel donut) { | |
selectedDonut = donut; | |
Utils.mainAppNav.currentState!.pushNamed( | |
'/details', | |
); | |
} | |
DonutService() { | |
selectedDonutType = filterBarItems.first.id; | |
// Execute the filteredDonutsByType in the DonutService's | |
// constructor, using the default value assigned to | |
// selectedDonutType from the first item in the filterBarItems | |
// collection, and assigning it to the filteredDonuts collection. | |
filteredDonutsByType(selectedDonutType!); | |
} | |
void filteredDonutsByType(String type) { | |
selectedDonutType = type; | |
// In the existing method called filteredDonutsByType, right | |
// after we assigned the selectedDonutType with the currently | |
// selected filter bar item from the DonutFilterBar widget, | |
// perform a filter against the existing mocked data in the | |
// Utils.donuts, and retrieved a filtered list of the donuts | |
// that match their type against the selectedDonutType; for this, | |
// use the existing type property from the DonutModel, then | |
// assign the resulting list to the filteredDonuts collection. | |
filteredDonuts = | |
Utils.donuts.where((d) => d.type == selectedDonutType).toList(); | |
notifyListeners(); | |
} | |
} | |
// PODO | |
class DonutPage { | |
String? imgUrl; | |
String? logoImgUrl; | |
DonutPage({ | |
this.imgUrl, | |
this.logoImgUrl, | |
}); | |
} | |
class DonutFilterBarItem { | |
String? id; | |
String? label; | |
DonutFilterBarItem({ | |
this.id, | |
this.label, | |
}); | |
} | |
class DonutModel { | |
String? imgUrl; | |
String? name; | |
String? description; | |
double? price; | |
String? type; | |
DonutModel({ | |
this.imgUrl, | |
this.name, | |
this.description, | |
this.price, | |
this.type, | |
}); | |
} | |
// UTILS | |
class Utils { | |
static GlobalKey<NavigatorState> mainListNav = GlobalKey(); | |
static GlobalKey<NavigatorState> mainAppNav = GlobalKey(); | |
static const Color mainColor = Color(0xFFFF0F7E); | |
static const Color mainDark = Color(0xFF980346); | |
static const String donutLogoWhiteNoText = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_shop_logowhite_notext.png'; | |
static const String donutLogoWhiteText = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_shop_text_reversed.png'; | |
static const String donutLogoRedText = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_shop_text.png'; | |
static const String donutTitleFavorites = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_favorites_title.png'; | |
static const String donutTitleMyDonuts = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_mydonuts_title.png'; | |
static const String donutPromo1 = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_promo1.png'; | |
static const String donutPromo2 = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_promo2.png'; | |
static const String donutPromo3 = | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donut_promo3.png'; | |
static List<DonutModel> donuts = [ | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutclassic/donut_classic1.png', | |
name: 'Strawberry Sprinkled Glazed', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'classic'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutclassic/donut_classic2.png', | |
name: 'Chocolate Glazed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'classic', | |
), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutclassic/donut_classic3.png', | |
name: 'Chocolate Dipped Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'classic'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutclassic/donut_classic4.png', | |
name: 'Cinamon Glazed Glazed', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'classic'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutclassic/donut_classic5.png', | |
name: 'Sugar Glazed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'classic'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutsprinkled/donut_sprinkled1.png', | |
name: 'Halloween Chocolate Glazed', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'sprinkled'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutsprinkled/donut_sprinkled2.png', | |
name: 'Party Sprinkled Cream', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'sprinkled'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutsprinkled/donut_sprinkled3.png', | |
name: 'Chocolate Glazed Sprinkled', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'sprinkled'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutsprinkled/donut_sprinkled4.png', | |
name: 'Strawbery Glazed Sprinkled', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'sprinkled'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutsprinkled/donut_sprinkled5.png', | |
name: 'Reese\'s Sprinkled', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 3.99, | |
type: 'sprinkled'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutstuffed/donut_stuffed1.png', | |
name: 'Brownie Cream Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'stuffed'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutstuffed/donut_stuffed2.png', | |
name: 'Jelly Stuffed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.99, | |
type: 'stuffed'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutstuffed/donut_stuffed3.png', | |
name: 'Caramel Stuffed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 2.59, | |
type: 'stuffed'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutstuffed/donut_stuffed4.png', | |
name: 'Maple Stuffed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.99, | |
type: 'stuffed'), | |
DonutModel( | |
imgUrl: | |
'https://romanejaquez.github.io/flutter-codelab4/assets/donutstuffed/donut_stuffed5.png', | |
name: 'Glazed Jelly Stuffed Doughnut', | |
description: | |
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit, tellus condimentum cursus gravida, lorem augue venenatis elit, sit amet bibendum quam neque id sapien.', | |
price: 1.59, | |
type: 'stuffed') | |
]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Finished this 17-05-2023.