What You'll Build in this Workshop:

In this part of the workshop (pt.2) we will tackle the landing page (see image below). If you are here, make sure you have completed pt.1 of this series.

Landing Page

We will tackle several aspects during this codelab for this page in particular:

First things first, we want to establish a good foundation for the landing page (which we're calling DonutShopMain), since this is where many of our feature widgets will reside (and even communicate with one another in a decoupled fashion).

This is what we'll set up initially as the base:

Landing Page

Let's continue expanding on our landing page DonutShopMain, where in the previous codelab we created it as a dummy page widget. We'll flesh it out more now.

Let's start from the top of the Scaffold, where we'll create the application bar. See the schematics below:

Landing Page

Now, let's start coding it!

In the DonutShopMain widget's Scaffold widget, set its appBar to a new instance of AppBar:

Landing Page

Proceed to set the rest of the options as follows:

Landing Page

Running the app at this point should get you the following output:

Landing Page

Landing Page

We'll be adding a Drawer widget (like a sliding side menu triggered by a hamburger menu) by first creating a simple widget that encapsulates this functionality. You can expand further on this side menu - we'll just add a couple of images on a background, just to show you how to implement it and then append it to the Scaffold.

Create a new custom widget that extends StatelessWidget called DonutSideMenu. Override its build method and return an empty Container for now:

Landing Page

Let's dig into its structure. To the existing Container, set its color property to a predefined one (Utils.mainDark), and add 40px of padding all around it:

Landing Page

As the child of this Container, add a Column with two Image widgets:

Landing Page

Make sure both images are left aligned and spread apart vertically by setting the Column properties of crossAxisAlignment and mainAxisAlignment to *start** and spaceBetween respectively:

Landing Page

To the top image, wrap it inside a Container so we can add a top margin of 40px to give it more breathing room:

Landing Page

I think we're pretty much done with it. Now let's go ahead and use it in our Scaffold.

Set the drawer property of the Scaffold to a Drawer widget, and set the child of the drawer widget our newly created DonutSideMenu widget, as such:

Landing Page

Running it on Dartpad, we should see the following on the preview panel:

Landing Page

We see the hamburger menu show once we set the Drawer, and tapping on it expands our newly created widget. Sweet! Let's proceed.

Landing Page

We want to distribute the Scaffold's body appropriately so it can accommodate the child pages that will be displayed in the landing page child stack as well as the bottom navigation bar that will trigger the swapping / navigating among those pages.

We'll distribute the body into the main content, for which we'll use an Expanded widget so it can take up most of the real estate of the body, followed by a placeholder Container widget (that will later be replaced by the bottom navigation bar).

Replace the contents of the Scaffold's body (currently a Center widget wrapping a Text widget) by a Column, holding an Expanded and a placeholder Container widget, as follows:

Landing Page

We should be all set to proceed to the next step, which is creating the bottom navigation bar for the landing page.

Landing Page

This widget will reflect which bar item is selected, trigger the child navigation stack so as to display the corresponding associated child page, as well as keep count of how many items have been added to the shopping cart (a nice-to-have we will implement when we get to the shopping cart functionality).

Let's proceed!

As before, we'll create a custom widget for this bottom bar to achieve a unique experience. Create a class called DonutBottomBar that extends StatelessWidget. Override its build method and return a Container with 30px padding all inside.

Landing Page

As the child of the Container, add a Row widget, with its crossAxisAlignment set to end and mainAxisAlignment set to spaceBetween so its children are aligned at the bottom, and spread apart:

Landing Page

Now, as the children of the Row widget, set three IconButton widgets, with the icon property set to your desired icons, with color Utils.mainColor initially, and the onPressed event set to empty (since its mandatory):

Landing Page

This is a good spot for us to test what we've done so far.

Go back to the DonutShopMain page widget and replace the placeholder Container widget below the Expanded widget:

Landing Page

Running it on DartPad, it should look like this once plugged in:

Landing Page

Now, let's add the selection functionality.

Now in order to persist the selection of the currently selected item in the bottom bar, we can tackle it multiple ways:

Let's proceed.

Import the provider service (IGNORE IF USING DARTPAD)

In your project's pubspec.yaml add a dependency to the provider package. Add the latest version to it:

Landing Page

Perform a flutter pub get or save the pubspec.yaml to trigger an update to its dependencies.

Import the provider package in your implementing classes

Whenever you're using any of the Provider classes (Provider, ChangeNotifierProvider, ChangeNotifier, Consumer, etc.) then import the provider package:

import 'package:provider/provider.dart';

Create a Provider Service to hold the Bottom Bar Selection

Create a class called DonutBottomBarSelectionService that extends ChangeNotifier. 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.

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.

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.

See the class implementation below:

class DonutBottomBarSelectionService extends ChangeNotifier {

  String? tabSelection = 'main';

  void setTabSelection(String selection) {
    tabSelection = selection;
    notifyListeners();
  }
}

Inject the Provider Service at the Root of the App

In order to give this provider service a global scope so it "provides" itself or trickles down to interested widgets in the hierarchy, you have to lift the state as high as you can; in our case we'll inject it at the very top - at the MaterialApp level. This sets app-level scope, as opposed to scoping it to the widget in which it is used.

Since we will be using more than one provider service in this app, let's use a MultiProvider widget, otherwise for a single-provider app or a locally-scoped service, a Provider or ChangeNotifierProvider widget would do.

Wrap the MaterialApp inside a MultiProvider widget, and set its providers property to an empty array to start with (and make sure to import the provider package):

Landing Page

Let's add our first provider service, the DonutBottomBarSelectionService wrapped inside a ChangeNotifierProvider object:

Landing Page

Consume the Service in the DonutBottomBar widget

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.

We'll put the Consumer widget inside the DonutBottomBar widget, more precisely around the Row widget that holds our bar items (IconButtons), that way when the selection changes (when the user taps on the corresponding IconButton

onPressed event), we appropriately set the selected one, which triggers the notifyListeners, which in turns forces itself to rebuild; then upon the widget rebuilding, the new selected tab value gets evaluated, and we change the color of the selected one accordingly.

Landing Page

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.

Now, let's make each one of the IconButton widgets trigger the Consumer to rebuild, by using the provided service instance (bottomBarSelectionService) and its provided method setTabSelection and passing the corresponding value of the selected tab item:

Landing Page

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. Now how do we evaluate the selected value upon each rebuild, and in turn change the color of the Icon widget in the IconButton widget accordingly?

Simply read the tabSelection property on each turn of the build method, and change the color of the Icon whether the item in question matches the selection, as such (apply this to all Icon widgets for each IconButton widget accordingly):

Landing Page

Running on DartPad to test the selection, notice how when each IconButton is clicked, the color of the child Icon changes to dark when it is the one being selected, while the rest change to a lighter color, efficiently proving that the state is being lifted and maintained outside of the widget, regardess of it rebuilding on every click.

Landing Page

Nicely done, and great introduction to State Management using Provider in Flutter!

Let's keep rolling!

We will now implement a way in which, without leaving the DonutShopMain landing page widget, we can swap out content, by using the Navigator widget, which sets up a child navigation stack private to this page, but triggered by an external component (the DonutBottomBar widget).

Landing Page

When a user clicks on a bottom bar navigation item, it will push a new child page onto the nested navigation stack in our landing page. The Navigator widget in the DonutShopMain page will switch between three sub-pages (main list, favorites list, shopping list). Let's see how it's done!

Landing Page

The main application contains its own Navigation Stack - that's how it accomplish navigating between the pages it's composed of (in our case, the SplashPage widget and the DonutShopMain pages are pushed onto the main app navigation stack - even the Drawer gets pushed to the main navigation stack ;) - go figure!).

Pages get pushed (in a stack fashion) one on top of the other, the topmost page being the one the user sees at a given time. Pages get popped off the top of the stack, revealing the one underneath until there's no more pages to pop, and so on.

However, pages can implement their own navigation stack - allowing child pages to be displayed within them with the help of the Navigator widget, which works similar to the main app navigation stack, allowing pages to be pushed onto it, etc. More on nested navigation here.

Set Up the Plumbing for the Navigation Strategy

We will have to do some refactoring in order to manage both navigation stacks (main nav stack, and landing page nav stack) independently, but efficiently.

Each navigation stack will be referenced by a unique global key, so that we can uniquely reference them and be able to push and pop widgets on and off of it efficiently.

In the Utils class we created earlier, start by defining two GlobalKey instances, each of which will be assigned to each navigation stack:

Landing Page

Refactor the Main Navigation / Set up Routes

Let's do some refactoring to the main navigation strategy, and instead of pushing instances of Route that get pushed onto the navigation stack, we will instead use named routes.

Go to the MaterialApp widget. Remove the home property which points to the SplashPage widget. Add the following properties:

Your code should look like this afterwards:

Landing Page

Let's take this for a spin! In the SplashPageState class, we are currently pushing the DonutShopMain page by creating a Route instance which wraps the corresponding widget, then pushing it onto the navigation stack.

Replace this by using the named route approach; use Utils.mainAppNav.currentState!.pushReplacementNamed(‘/main'), which pushes the widget page associated with the route key named "main", hence beign a "named" route. Your navigation code should look like this:

Landing Page

Running this code should not display anything different from what it was before - the SplashPage gets displayed, then after 2 seconds, it navigates to the DonutShopMain page, this time being done via a named route, but yielding the same results.

And with that in place, we are ready to set up the Navigator widget and our nested navigation strategy!

Add the Navigator Widget to the DonutShopMain Landing Page

Let's go to the DonutShopMain widget page; inside the Expanded widget, replace the child Container widget by a Navigator widget.

Landing Page

Inside the Navigator widget, set its key property (Utils.mainListNav) and its initialRoute ("/") - just like the main navigator (yes, even a child navigator has an initialRoute so it knows which page to land on initially by default). We'll set up its routes as well.

Landing Page

In order to set up the routes managed by this Navigator widget, you must hook up to its onGenerateRoute callback, which gets triggered when a user pushes a named route onto this child navigation stack, as follows:

Landing Page

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.

Below the whole code that should go inside the onGenerateRoute (we'll implement each route's corresponding page individually later on):

Landing Page

Time to kick the tires on this nested navigation!

Pushing named routes onto the DonutShopMain child navigation stack

We said earlier that the DonutBottomBar widget, upon the user tapping on one of the items, it will push the corresponding named route. Let's leverage the existing functionality of the DonutBottomBar widget that calls the setTabSelection method on the DonutBottomBarSelectionService and use the same tabSelection as the name of the named route to be pushed onto the child navigation stack, as follows:

Landing Page

With this, we trigger a change in the child navigation stack in the DonutShopMain page (via the Utils.mainListNav.currentState!.pushReplacementNamed using the same tab selection) while at the same time preserving the tab selection and triggering a rebuild on the Consumer widget. One single line of code change, while no changes on the bottom bar widget. See it in action!

Landing Page

Notice how the pages get swapped as the user taps on the corresponding bottom bar item.

With the nested navigation in place, we are free to flesh out each of the child pages in the main landing page: the DonutMainPage, DonutFavoritesPage and the DonutShoppingCartPage.

Let's proceed!

Landing Page

Now we can concentrate on each of the individual child pages that get displayed within the Navigator widget inside our DonutShopMain landing page. This time the focus is on the DonutMainPage.

This page will be comprised of 3 widgets:

Set Up The Layout for the DonutMainPage

We'll get some things out of the way first by setting up the layout of the DonutMainPage child page widget.

Start by creating a custom widget class called DonutMainPage that extends StatelessWidget. Override its build method as customary and return a Column with empty children array as default. As shown above, our child widgets will be laid out vertically:

Landing Page

Then, go to the Navigator widget in the DonutShopMain page, and in the onGenerateRoute callback, find the switch case that matches the /main named route; replace the placeholder Center widget by our newly created DonutMainPage custom child page widget, as such:

Landing Page

With that hooked up, we are ready to expand and define this child page further. Click Next to proceed.

Let's start with the DonutPager. The following image is a schematic view of what we'll be tackling:

Landing Page

Proceed to create a custom widget called DonutPager, this time we'll make it straight into a StatefulWidget since there will be some internal state to be maintained, also we will be using the PageView widget, which requires a PageController that needs to clean up its resources afterwards.

Landing Page

Create a PODO model to feed mock data into the DonutPager widget

In order to feed the images that make up our DonutPager widget, we pull them from a remote location. I want to package them nicely in a single object, for which I'll create a small PODO (Plain Ol' Dart Model) class called DonutPage, which will have two properties:

Landing Page

With that out of the way, let's focus our attention back to our DonutPager. Let's create a collection of DonutPage objects inside our DonutPagerState widget to mock the retrieval of several images to display on our page view.

Landing Page

Create two more properties:

Landing Page

Override the initState method and initialize the controller instance, setting its initialState property to zero:

Landing Page

Landing Page

Now let's focus our attention inside the DonutPager's build method, and let's build its structure.

Replace the placeholder Container widget by a SizedBox widget, with a fixed height of 350px. We want to constrain the height of this widget to a fixed dimension. Add a Column widget as its child with an empty children array by default:

Landing Page

Inside of this Column widget is where we want to place the PageView widget and later on our custom PageViewIndicator widget; we want the PageView to occupy most of the Column's real estate, so let's start by an Expanded widget, which will serve as the PageView's wrapper / parent, then add the PageView widget as its child:

Landing Page

Let's set some properties on the PageView widget:

Landing Page

Time to load the children on this PageView widget! We'll use the PageView's children property to feed a list of DonutPage objects (which we've conveniently created and set up as a List of DonutPage objects a few steps back).

We'll conveniently use the List.generate factory method with this list to generate widgets on the fly based on how many DonutPage objects we have, and feed the data into custom-created widgets containing the image information. To the List.generate method, pass the amount of pages available (*pages.lenght) and a callback which we'll use to tap into each item in the iteration, and in turn build each page in the PageView, as follows:

Landing Page

Build the structure of each PageView page

Now let's focus our attention on the List.generate's callback method. This 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, as such:

Landing Page

Let's start adding some properties to this Container widget:

Landing Page

We'll use the Container widget as a canvas to paint the promo images as a background image, as well as add roundness to the Container and some shadow effects to give it some depth. To accomplish all this this we'll use the decoration property of Container, which we'll facilitate all these features. We'll set the following properties of the decoration, type BoxDecoration:

After setting the decoration properties, our Container code should look like this afterwards:

Landing Page

Let's test what we've built so far.

Back on our DonutMainPage, add our newly created DonutPager as the first child of the Column widget in here, as follows:

Landing Page

Running what we have so far on DartPad, and our preview panel should show the following:

Landing Page

Nice, our DonutPager widget is in place. The last touch is to add the little image logo right on top of the Container's background, which is as simple as adding it as a child of that same Container. Go back to the Container being generated as each PageView page, then pull the logo image out of the currentPage model's logoImgUrl property, and set it to an Image.network widget, with a 120px width, as shown below:

Landing Page

Taking it for a spin once again, and we should see the logo image showing on top of the background image. Play with swiping left and right, and testing the snapping functionality. Sweet!

Landing Page

Let's keep moving!

Let's add a nice touch to this DonutPager - a way for users to see how many pages they will be viewing, and a way to let them know how far along they are on the list by using some circular indicators. We'll encapsulate this functionality in a very convenient widget called PageViewIndicator.

Check out the schematics of this widget below:

Landing Page

Let's start by creating a new custom widget class called PageViewIndicator that extends StatelessWidget. Override its build method and return a Row widget with an empty children array by default:

Landing Page

Next, we'll create 3 convenient properties:

Pass the values via this widget's constructor, and your code should look as follows:

Landing Page

Build the Structure of the PageViewIndicator

Let's focus now on the build method of this widget. We want our circular indicators to be centered in the row, so let's set its mainAxisAlignment property to center.

Landing Page

To populate each circular indicator based on the number of pages available, let's populate the Row's children property through the List.generate factory method, passing the numberOfPages as the length of items to iterate on, and the callback that will spit out each indicator in turn, as follows:

Landing Page

Now let's give more definition to the Container that will represent each page indicator.

Let's start adding some properties to give this Container the circular shape and color we want. Start by adding the following:

Your code should look as follows afterwards:

Landing Page

Let's start plugging this widget in place.

Back on our DonutPagerState widget, inside the build method, locate the Expanded widget that is wrapping our PageView widget. Right underneath it, add our newly created PageViewIndicator widget. Pass the required values, as illustrated below:

Landing Page

Running this on DartPad, and you should see the page indicator circles showing up! Hooray!!!

Landing Page

Wait - no so fast. They are showing alright, but when you slide left and right, the indicators don't show what the current page - is as if this control is static. We want it so that when we swipe on the PageView widget, the PageViewIndicator also reflects the index of which page is currently showing.

For this, you need to tap into the PageView widget's onPageChanged event. This event gets triggered every time a user swipes on the specified direction, thus triggering a change on the page. This gets assigned a callback, which supplies a parameter of type int that represents the current page being shown.

What we'll do is tap into this callback, capture the index of the current page being displayed, and trigger a widget rebuild so the PageViewIndicator rebuilds as well, reflecting the correct current page being displayed.

In the PageView widget, hook up a callback to the onPageChanged event, and inside this callback, trigger a rebuild using the setState method available on this State class. Inside of this method, then set the currentPage property using the page property supplied by the callback, as follows:

Landing Page

Now with these latest changes, after swiping left and right, you should see the indicator changing! Now we are talking!

Landing Page

BONUS #1: Add More Interactivity to the PageViewIndicator

Cool that this widget shows the currently displayed page from the PageView. What if I want to use those same indicators to take me to the corresponding page I want to see? So simple - let me show you.

Back in your PageViewIndicator widget, wrap the Container that represents the indicator inside a GestureDetector, since we want to make this Container tappable.

Landing Page

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.

At the end your code should look like below:

Landing Page

BONUS #2: Animate the Coloring of the Indicators in the PageViewIndicator

We can also add some animation to the color changing effect in the PageViewIndicator individual indicators. A nice-to-have, that kind of smoothens out the animation. Let's do it real quick.

Back to that same Container that represents each indicator widget, replace the Container by an AnimatedContainer widget. The AnimatedContainer is one of those explicit animation widgets available in the library that does all the heavy lifting for you when it comes to animate any of the properties available on a Container. In our case, we want to animate the color property, which is the only thing that changes between widget rebuilds.

Replace Container by AnimatedContainer, and supply two additional properties to get it going:

After applying the AnimatedContainer, your code should look like this:

Landing Page

Run it once again and you'll notice the color animation, which looks much smoother and slicker! Kudos to you!

Landing Page

And that about wraps up the DonutPager which included the PageViewIndicator. Click next to proceed to the next section.

Landing Page

The DonutFilterBar is a widget that handles the filtering of the list of donuts by their types (classic, sprinkled and stuffed). I made it custom so as to continue introducing State Management concepts as well as bringing more interactivity to your apps by introducing both explicit and implicit animations.

The following schematic view shows how we'll be tackling it:

Landing Page

Each filter bar item will perform the filtering of the list of donuts below, as well as change the color of the currently selected one, and perform an animation by showing a sliding bar that runs across the filter bar and lands on the currently selected bar item.

Let's proceed!

Creating the core DonutFilterBar widget

First things first - let's create the custom widget that will hold the filter bar, so create a class called DonutFilterBar and make it extend StatelessWidget:

Landing Page

Let's give a pause on building the structure of this widget to give way to building the mocked data and services required to hydrate this widget.

Creating the model that represents each Filter Bar Item

Let's create another useful PODO (Plain Ol' Dart Object) that will hold the data for each of the filter bar items. Let's call it DonutFilterBarItem. We are only interested in knowing the label to display, and the id to uniquely identify the currently selected filter bar item.

Landing Page

Create a Provider Service To Hold Donut Information

We need to create a provider service to hold all the logic of persisting the currently selected filter, the list of filters available, and later on we'll use it to perform the actual filter and hold the data of our available products.

Let's start by creating a provider service class called DonutService. This class will also extends ChangeNotifier since this will perform operations that may trigger widget rebuilds:

Landing Page

Landing Page

In our newly created service, create a mocked data collection representing the three categories of donuts, against which our users should be able to filter their donuts on. Make it a List of type DonutFilterBarItem and populate it accordingly:

Landing Page

Proceed to create a property called selectedDonutType, type string; in the service's constructor, set an initial value to be the first filter bar item id (*classic) as a default value. This property will hold the value of the selected filter bar item at any given point and we have a starting point for what to render the default selection with:

Landing Page

In this same service, let's create a convenient method to call when the user taps on each filter bar item to perform the actual filtering. We will only hold the selected filter bar item for now - later we'll perform the actual filtering. Subsequently, make a call to notifyListeners (provided by inheriting from ChangeNotifier) to trigger a widget rebuild in the event that the selectedDonutType has changed:

Landing Page

With our data mocked up and our provider service ready to be consumed, let's proceed and wrap up the DonutFilterBar widget structure.

Creating the DonutFilterBar widget structure

In the DonutFilterBar widget, replace the placeholder Container widget by a more specialized Padding widget - we only need to apply padding here, so there's no need for a full-blown Container. Apply padding all inside of 20px:

Landing Page

Since we'll be listening to changes on the currently selected filter bar item, which is being held in the DonutService provider service, let's go right ahead and create a Consumer widget. Recall that a Consumer widget will trigger its builder method when it calls notifyListeners internally - in our case, upon users tapping on the available filtering options. Make the Consumer widget a direct child of the Padding widget:

Landing Page

We must return something out of the Consumer's builder method, therefore if we go by the schematic graphic above, we must start by creating a Column widget:

Landing Page

This Column will hold the Row of filters, some spacing and the sliding bar.

Let's start from the top and add a Row widget, in which its items will be spaced out, hence setting its mainAxisAlignment to spaceAround:

Landing Page

This Row widget will hold the filter bar items, based on the filterBarItems collection of DonutFilterBarItem objects we mocked up earlier in the provided DonutService. Populate the children property of the Row by using the convenient factory method List.generate, feeding into it the filterBarItems collection and a callback that will generate each filter bar item accordingly:

Landing Page

Inside the List.generate callback, extract the corresponding DonutFilterBarItem out of the provided service donutService and its filterBarItems property, using the supplied index value through the callback, and holding it in a property called item:

Landing Page

Using each DonutFilterBarItem item, construct a Container widget that wraps a Text widget inside, with the correct item.label value as well as the corresponding color style, based on whether the currently selected filter bar item (stored in donutService.selectedDonutType) matches the item.id, as follows:

Landing Page

Let's take what we've build so far for a spin.

Plug in this widget into its final destination, by adding it right below the DonutPager widget inside the DonutMainPage child page widget:

Landing Page

Running this on DartPad to see what is looking like, should yield the following result:

Landing Page

At least you can see the currently selected default item (Classic) but there's no interactivity and no way to change the current selection. In the next step, we'll add more interactivity to it.

Add Interactivity to DonutFilterBar Upon Bar Item Tapping

Let's make them clickable now. Back to the DonutFilterBar, inside the List.generate callback of the Row widget, wrap each Container widget inside a GestureDetector widget to leverage its tapping capabilities:

Landing Page

Inside the GestureDetector's onTap event, invoke the donutService.filteredDonutsByType method, passing the corresponding item's id upon the user tapping on the filter bar item. This will persist the currently selected filter bar item, as well as trigger the parent Consumer widget, kicking off a rebuild, which eventually updates the selected filter bar item:

Landing Page

Running it again, and tapping on the different filter bar items available, should yield the following result:

Landing Page

Let's add yet more interactivity by adding the sliding bar below this Row widget.

Adding Sliding Bar below Filter Bar Item Row

The sliding bar effect under the Row widget is nothing fancy - is nothing more than a Container widget that animates its alignment between 3 available positions (left, center and right) over a short period of time in a smooth fashion. Let me show you how I did it.

Back in our DonutFilterBar widget, first add some spacing between the Row of filter bar item widgets and our upcoming sliding bar. Make it a SizedBox with a 10px height:

Landing Page

Right under this spacing widget, add a Stack widget.

Landing Page

We'll use a Stack which will occupy the whole width of the screen and will function as some sort of "rail" on which our Container widget will slide from left to right, aided by an AnimatedAlign widget, which will change its value from 3 possible options based on the selected filter bar item: left (Classic), center (Sprinkled) and right (Stuffed). Let's create a method that will handle this logic.

Create a local method to this widget called alignmentBasedOnTap that takes as a parameter the id of the selected filter bar item, and will perform the logic of returning the corresponding alignment based on the provided filter bar item id:

Landing Page

Back on the Stack, add as its only child inside the children collection, an AnimatedAlign widget, with the following properties:

Your code should look like this so far:

Landing Page

Now the only thing missing is the Container widget that will represent the bar that will simulate a sliding effect. Add as a child of the AnimatedAlign widget a Container widget with the following specs:

The whole AnimatedAlign widget, including its Container widget should look like this:

Landing Page

Running it one more time on DartPad in order to test the sliding effect, should show you this on the preview panel:

Landing Page

And that takes care of the DonutFilterBar widget in its entirety. You should feel proud of what you've accomplished and all we've learned in terms of widget composition, animations and state management!

Let's move on to the next and last widget of the DonutMainPage - the DonutList widget!

Let's check out the schematic view of the DonutList widget we'll be building in this section:

Landing Page

The DonutList will consist of an AnimatedList widget, which is an animated version of the ListView, a scrollable container of child widgets. Each of the child widgets will be a custom DonutCard that will display the information of our donut products.

At the end of this codelab, we will hook it up to the DonutFilterBar we created earlier so we can filter the list of donuts by the selected filter bar item (between Classic, Sprinkled and Stuff).

Let's proceed.

Set up the data to feed into the DonutList Widget

Just like we've done in the previous widgets, we'll use some mocked data to hydrate the list of widgets, in our case we need a collection of donut products, each of which may contain the image of the product, name, price and description. Notice where I'm headed? Yes, we will need a PODO object that will encapsulate this data.

Let's create a model class called DonutModel, which will contain the following properties:

Your model class should look like this:

Landing Page

Prepare mocked data in the Utils class

I've prepared some static mocked data collection representing a list of donut products for use within this app. Feel free to copy / paste the code below anywhere inside the Utils class we created earlier:

/* place inside the Utils class */

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'
    )
  ];

With that out of the way, let's create our custom DonutList widget.

Creating the core DonutList widget

Start by creating a custom widget class called DonutList, which inherits from StatefulWidget (we will make it StatefulWidget from the get-go since later we will introduce implicit animations which require tacking on to the State). Right out of the gate, make the corresponding State class (which you can call _DonutListState) return a ListView from its overriden build method (let's start with a ListView to get things going, later down the lab we'll turn it into an AnimatedList).

This should be your code:

Landing Page

Let's refactor the default constructor of the DonutList

Stateful widget to take a list of DonutModel objects, therefore create the corresponding class member to hold its value, as follows:

Landing Page

Let's continue. In the _DonutListState class, where the build method resides, replace the existing ListView by a ListView.builder; this is because the ListView.builder factory method resembles more the AnimatedList constructor for when we build the animated version of the list. Add the following properties to the ListView.builder:

Your code should look like this:

Landing Page

Let's add something to the itemBuilder callback so it stops barking (see the red squiggly line!). Let's come back to this method, and go ahead and build out the structure of each of the donut cards.

Creating the DonutCard widget

Inside of the ListView widget (and eventually the AnimatedList widget), we would like to display each donut as a separate widget, in a card-looking fashion that we'll custom create. Check out this quick schematic view of our card.

Landing Page

Notice the overlapping nature of some of its parts, to give it a unique effect. Users will eventually be able to tap on this card, which will reveal more details about this product by performing a navigation action. More on this on the next lab. Let's proceed with our custom card.

Creating the core DonutCard widget

Let's start by creating another custom widget class called DonutCard that extends StatelessWidget.

While overriding its build method, return a Stack widget - this will be the foundation widget of our structure:

Landing Page

Add an empty children array by default, and make sure that when we lay them out, they are center aligned, using the Alignment.center alignment option:

Landing Page

Since a Stack lays out his children in a layered fashion (bottom-up), let's lay down the first children, which is a Container - the one holding the name and price info.

Add a Container inside the Stack as its first child, with the following specs:

Your Container should look like this afterwards:

Landing Page

As the child of this Container, add a Column widget, with an empty children array to start with. We want our items to be aligned at the bottom of the column, left-aligned - therefore set the mainAxisAlignment to end, and crossAxisAlignment to start:

Landing Page

We want to display information related to a donut product, so where will we pull this info? Let's make some changes to this class before proceeding any further.

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, as such:

Landing Page

Now let's get back to the structure we were building earlier - the Column widget that will hold the text content, so let's focus on that column for a bit.

Add the first item in the Column: a Text widget that will display the name of the donut (pulling the data out of the injected donutInfo model via the constructor), with the following specs for its TextStyle:

Your Text widget should look like this:

Landing Page

Add some spacing underneath the Text widget by adding a SizedBox with 10px of height - give it some breathing room:

Landing Page

Now let's add the little badge to display the price information in. For that, we'll use a Container widget, with the following specs:

Your Container should look like this:

Landing Page

We need to display the price information inside this little badge Container widget by pulling the value from donutInfo.price, so assign a Text widget as the sole child of this Container, with the following specs:

Your Text widget inside the Container should look like this:

Landing Page

Test the DonutCard and DonutList (pt.1)

We've coded a lot - let's see how things are looking so far. We'll get back to finalizing the structure, but let's test what we've built up to this point.

Back up to the _DonutListState widget, inside the itemBuilder callback of the ListView.builder, let's use the DonutCard here. First, pull a DonutModel object out of the donuts list that we'll be injected via the constructor, using the index supplied through the callback. Hold the value in a variable called currentDonut:

Landing Page

Now, use this currentDonut to churn out DonutCard widgets out of this callback. Return a DonutCard widget, and pass the currentDonut into the constructor, as such:

Landing Page

With that hooked up, let's go back to the DonutMainPage and start putting the DonutList in place to test how's coming out. We'll even feed it the mocked donuts list straight up (Utils.donuts) just to see it in action. Add the DonutList widget right under the DonutFilterBar, feeding it the Utils.donuts assigning it to the donuts argument.

Since we're adding it to the main Column layout of the DonutMainPage widget, and we want it to take up most of the real estate in the column, wrap it inside an Expanded widget, as such:

Landing Page

Running what we have so far on DartPad, you should be able to see the following:

Landing Page

Now, let's finish up the DonutCard by adding the missing piece - the donut image. Recall that we added a Stack widget in order to achieve that staggered / overlapped effect between the image and the card content below. Let's do it!

Back to the DonutCard widget, locate the Stack widget. Right under the existing Container widget that holds the text information, we'll add an Image widget, using the Image.network option, but to achieve the staggered effect, we'll wrap the Image inside an Align widget with a top center alignment. Recall that the Container widget has a top margin of 80px in order to force the white space we'll be devoting to the image. Once the Align widget anchors itself to the top center of the Stack, it will have that overlapping effect.

Add the Image widget using the network option, with 150px in dimension, and a fit of BoxFit.contain to make sure it preserves its aspect ratio. Feed the image path by pulling it out of the donutInfo.imgUrl. Wrap the image inside an Align widget, with a topCenter alignment, as follows:

Landing Page

Re-run the app in DartPad and it should look pretty much done up to this point:

Landing Page

And if you scroll through all items available, notice we are showing them all - there's no filtering. Now we should be able to hook up the DonutFilterBar filtering functionality with the DonutList, so when the user taps on the desired filter bar item, the DonutList widget and its list of donuts filters itself to show only the selected item.

Landing Page

Let's work on that!

In order to have the DonutFilterBar communicate with the DonutList in a decoupled way, we'll leverage the DonutService we created earlier. When the filtering is triggered from the DonutFilterBar, the DonutList listens to that change and rebuilds itself accordingly, feeding into it the filtered donut list - and the glue between them is the DonutService.

Refactor the DonutService to pull donut data

Let's go to the DonutService and start by adding a new property - a List of type DonutModel called filteredDonuts that we'll use to hold the filtered list of donut products at any given time. Initialize it with an empty array for now:

Landing Page

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:

Landing Page

Notice that we still keep the notifyListeners call at the end of this method; this will ensure that when the DonutList rebuilds itself when listens to a change in this service. All we're missing to do is make the DonutList widget listen for changes occurring on the DonutService, and what better way to do it than make it a "consumer" of the Donutservice.

Last thing to do is take care of the initial load, since initially the user hasn't selected anything!. For this, we will 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, also as a default initial filtered collection, as follows:

Landing Page

Back in the DonutMainPage, locate the DonutList widget, wrapped inside an Expanded widget. All we need to do is wrap this widget into a Consumer widget that listens to the DonutService, and feeds the updated filteredDonuts into the DonutList upon rebuilding, accurately showing the shorten list of donuts, matching what the user selected, as such:

Landing Page

Test the DonutCard and DonutList (pt.2)

Initially you should see the filtered donut list only showing the "classic" donuts - matching the default selection on the DonutFilterBar. Now tap on any of the other filter bar items; the selection gets stored in the DonutService, which does the filtered based on this selection; stores the new filtered values internally and calls notifyListeners so any interested listeners (in our case, DonutList) rebuild themselves and show the relevant information.

Run it again and tap around, you should see it working as expected - performing the filtering! Eureka!!!

Landing Page

If you want to add yet more interactivity and slickness to your app while giving it a nice touch and make it stand from the rest (and on the cheap!), animations is the way to go.

You saw what we did on the DonutFilterBar as well as the DonutPager, where even the slightest detail made a noticeable difference. We'll do the same with the DonutList items by changing the way they get shown on the list and add some sort of animated transition as they come in.

Landing Page

As you saw in the image provided, the list items get displayed with a fading and sliding animated transition as they enter, so you can imagine that these two animations should be happening in parallel - as it slides in, it starts to fade in. That's exactly what we will do: add a SlidingTransition and FadeTransition widget!

Both the SlidingTransition and FadeTransition widgets are part of the explicit animations package that comes out of the box with Flutter. At a fundamental level, explicit animations provide you with controls for telling Flutter how to quickly rebuild a widget tree to create the illusion of motion.

Refactor the DonutList ListView

We'll refactor the existing ListView.builder and replace it by an AnimatedList widget. The **AnimatedList widget consists of a scrolling container that animates items as they are inserted or removed. We will leverage the animated capabilites of the AnimatedList by inserting the items in a staggered fashion in order to achieve that animation where they gradually appear into view.

First, let's 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.

Landing Page

Let's also create a collection of DonutModel references that will serve as the holding collection where we'll be inserting the items in a staggered manner from the original donuts collection:

Landing Page

Next, go to the build method and do the following replacements:

Your new AnimatedList code should look like this:

Landing Page

Let's now implement the logic of how to add the items to the insertedItems list in a staggered fashion. We'll achieve it by introducing a small delay between inserts (about 125 milliseconds). We'll use Futures for this.

Override the initState method in the _DonutListState class, so we can do some needed initializations:

Landing Page

Inside of the initState method, create a loop that runs for the length of provided donut items in the widget.donuts collection, and for each iteration, create a Future object with a 125 ms. delay, inserted in the holding collection insertedItems as well as insert the corresponding index directly to the AnimatedList (via the GlobalKey reference, i.e. key.currentState!.insertItem). This is done so that the AnimatedList knows how to reference how many items are in its internal collection in regards to the actual collection of list items to be animated:

Landing Page

To recap: what will happen is that:

If you try to run the code as-is now, you'll see them pop in, one after the other, with a 125 ms. time difference from each other, since there is no animation.

Landing Page

Let's now implement the animation and leverage all we've done so far. In order to achieve the desired effect, we will have to wrap our DonutCard inside a FadeTransition inside a SlideTransition. Let's see how it's done.

Back in the build method of the DonutList widget, inside the AnimatedList

itemBuilder callback, wrap the DonutCard being returned inside a FadeTransition widget:

Landing Page

Let's dissect this code for a sec:

Running the code as-is, shows us just the fading transition - the items show one at a time, at least in a smooth fading fashion.

Landing Page

Let's wrap it up by adding the final touch, which is adding the sliding animation. In order to make the animations run in parallel, just wrap one inside the other. In this case, wrap the current FadeTransition into a SlideTransition widget, as follows:

Landing Page

Dissecting the code as well, it is pretty similar to the FadeTransition widget, with the only difference that the Tween object takes an Offset (an x and y coordinate in a 2D plane) object instead of a single value (1 or 0, for opacity).

What we're stating by providing a Tween with a begin (0.2, 0.0) and an end of (0.0, 0.0) is that we want a sliding transition, where the x-axis moves from 0.2 (20% of its original position) back to 0 (its original position), while the y-axis remains constant during the transition.

Running this code once again on DartPad and you should see the full-blown animation, both sliding and fading, as the items enter the screen (behind the scene getting inserted in real-time ;)), giving it a nice animated transition.

Landing Page

And with that we've concluded the coding of our landing page. In the next codelab we will tackle the DonutShopDetails widget page, so keep pushing along!

Thank you for making this far - hope you've enjoyed these codelabs so far!

And with that, we wrap up codelab #2 of this series, where we accomplished the following:

In codelab #3 of this series, we'll be tackling the details page of this application, so continue your journey!

Please don't forget to follow me on social media:

In case you fell behind on this codelab, below is the whole code for this codelab in a way you can copy / paste directly into DartPad:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => DonutBottomBarSelectionService(),
        ),
        ChangeNotifierProvider(
          create: (_) => DonutService(),
        )
      ],
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        initialRoute: '/',
        navigatorKey: Utils.mainAppNav,
        routes: {
          '/': (context) => SplashPage(),
          '/main': (context) => DonutShopMain()
        }
      )
    )
  ); 
}

class SplashPage extends StatefulWidget {

  @override 
  SplashPageState createState() => SplashPageState();
}

class SplashPageState extends State<SplashPage> 
  with SingleTickerProviderStateMixin {

  AnimationController? donutController;
  Animation<double>? rotationAnimation;

  @override
  void initState() {
    super.initState();
    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(const Duration(seconds: 2), () {
      Utils.mainAppNav.currentState!.pushReplacementNamed('/main');
    });

    return Scaffold(
      backgroundColor: Utils.mainColor,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            RotationTransition(
              turns: rotationAnimation!,
              child: Image.network(Utils.donutLogoWhiteNoText, width: 100, height: 100),
            ),
            Image.network(Utils.donutLogoWhiteText, width: 150, height: 150)
          ],
        ),
      )
    );
  }
}

class DonutShopMain extends StatelessWidget {

  @override 
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: Drawer(
        child: DonutSideMenu()
      ),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Utils.mainDark),
        backgroundColor: Colors.transparent,
        elevation: 0,
        centerTitle: true,
        title: Image.network(Utils.donutLogoRedText, width: 120)
      ),
      body: Column(
        children: [
          Expanded(
            child: Navigator(
              key: Utils.mainListNav,
              initialRoute: '/main',
              onGenerateRoute: (RouteSettings settings) {
                Widget page;
                switch(settings.name) {
                  case '/main':
                    page = DonutMainPage();
                    break;
                  case '/favorites':
                    page = Center(child: Text('favorites'));
                    break;
                  case '/shoppingcart':
                    page = Center(child: Text('shopping cart'));
                    break;
                  default:
                    page = Center(child: Text('main'));
                    break;
                }

                return PageRouteBuilder(pageBuilder: (_, __, ___) => page,
                  transitionDuration: const Duration(seconds: 0)
                );
             }
             
           )


          ),
          DonutBottomBar()
        ]
      )
    );
  }
}

class DonutMainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        DonutPager(),
        DonutFilterBar(),
        Expanded(
          child: Consumer<DonutService>(
            builder: (context, donutService, child) {
              return DonutList(donuts: donutService.filteredDonuts);
            },
          )
        )
      ]
    );
  }
}

class DonutPager extends StatefulWidget {
  @override
  State<DonutPager> createState() => _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? controller;

  @override
  void initState() {
    controller = PageController(initialPage: 0);
    super.initState();
  }

  @override
  void dispose() {
    controller!.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 350,
      child: Column(
        children: [
          Expanded(
            child: PageView(
              scrollDirection: Axis.horizontal,
              pageSnapping: true,
              controller: controller,
              onPageChanged: (int page) {
                setState(() {
                  currentPage = page;
                });
              },
              children: List.generate(pages.length, (index) {
                DonutPage currentPage = pages[index];
                return Container(
                  alignment: Alignment.bottomLeft,
                  margin: EdgeInsets.all(20),
                  padding: EdgeInsets.all(30),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(30),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(0.2), 
                        blurRadius: 10, 
                        offset: Offset(0.0, 5.0)
                      )
                    ],
                    image: DecorationImage(
                      image: NetworkImage(currentPage.imgUrl!),
                      fit: BoxFit.cover
                    )
                  ),
                  child: Image.network(currentPage.logoImgUrl!, width: 120)
                );
              })
            )
          ),
          PageViewIndicator(
            controller: controller, 
            numberOfPages: pages.length,
            currentPage: currentPage,
          )
        ],
      )
    );
  }
}

class PageViewIndicator extends StatelessWidget {
  
  PageController? controller;
  int? numberOfPages;
  int? currentPage;

  PageViewIndicator({ this.controller, this.numberOfPages, this.currentPage });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(numberOfPages!, (index) {

        return GestureDetector(  
          onTap: () {
            controller!.animateToPage(
              index, 
              duration: const Duration(milliseconds: 500), 
              curve: Curves.easeInOut);
          },
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 250),
            curve: Curves.easeInOut,
            width: 15,
            height: 15,
            margin: EdgeInsets.all(10),
            decoration: BoxDecoration(
              color: currentPage == index ? 
                Utils.mainColor : Colors.grey.withOpacity(0.2),
              borderRadius: BorderRadius.circular(10)
            )
          )
        );

        
      })
    );
  }
 
}

class DonutSideMenu extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Utils.mainDark,
      padding: EdgeInsets.all(40),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Container(
            margin: EdgeInsets.only(top: 40),
            child: Image.network(Utils.donutLogoWhiteNoText,
              width: 100
            )
          ),
          Image.network(Utils.donutLogoWhiteText,
            width: 150
          )
        ],
      )
    );
  }
}

class DonutBottomBar extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(30),
      child: Consumer<DonutBottomBarSelectionService>(
        builder: (context, bottomBarSelectionService, child) {
          return Row(
            crossAxisAlignment: CrossAxisAlignment.end,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              IconButton(
                icon: Icon(
                  Icons.trip_origin, 
                  color: bottomBarSelectionService.tabSelection == 'main' ? 
                            Utils.mainDark : Utils.mainColor
                ),
                onPressed: () {
                  bottomBarSelectionService.setTabSelection('main');
                }
              ),
              IconButton(
                icon: Icon(Icons.favorite, 
                color: bottomBarSelectionService.tabSelection == 'favorites' ? 
                            Utils.mainDark : Utils.mainColor
                ),
                onPressed: () {
                  bottomBarSelectionService.setTabSelection('favorites');
                }
              ),
              IconButton(
                icon: Icon(Icons.shopping_cart,
                color: bottomBarSelectionService.tabSelection == 'shoppingcart' ? 
                            Utils.mainDark : Utils.mainColor
                ),
                onPressed: () {
                  bottomBarSelectionService.setTabSelection('shoppingcart');
                }
              )
            ]
          );
      })
    );
  }
}

class DonutFilterBar extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(20),
      child: Consumer<DonutService>(
        builder: (context, donutService, child) {
          return Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: List.generate(
                  donutService.filterBarItems.length, (index) {

                    DonutFilterBarItem item = donutService.filterBarItems[index];

                    return  GestureDetector(
                      onTap: () { 
                        donutService.filteredDonutsByType(item.id!);
                      },
                      child: Container(
                        child: Text('${item.label!}', 
                        style: TextStyle(
                          color: donutService.selectedDonutType == item.id ? 
                          Utils.mainColor : Colors.black, fontWeight: FontWeight.bold)
                        )
                      )
                    );
                  }
                )
              ),
              SizedBox(height: 10),
              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,
                      height: 5,
                      decoration: BoxDecoration(
                        color: Utils.mainColor,
                        borderRadius: BorderRadius.circular(20)
                      )
                    )
                  )
                ],
              )
            ]
          );
        }
      )
    );
  }

  Alignment alignmentBasedOnTap(filterBarId) {
    
    switch(filterBarId) {
      case 'classic':
        return Alignment.centerLeft;
      case 'sprinkled':
        return Alignment.center;
      case 'stuffed':
        return Alignment.centerRight;
      default:
        return Alignment.centerLeft;
    }
  }
}

class DonutList extends StatefulWidget {
  List<DonutModel>? donuts;

  DonutList({ this.donuts });

  @override
  State<DonutList> createState() => _DonutListState();
}

class _DonutListState extends State<DonutList> {
  final GlobalKey<AnimatedListState> _key = GlobalKey();
  List<DonutModel> insertedItems = [];

  @override 
  void initState() {
    super.initState();

    var future = Future(() {});
    for (var i = 0; i < widget.donuts!.length; i++) {
      future = future.then((_) {
        return Future.delayed(const Duration(milliseconds: 125), () {
          insertedItems.add(widget.donuts![i]);
          _key.currentState!.insertItem(i);
        });
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedList(
      key: _key,
      scrollDirection: Axis.horizontal,
      initialItemCount: insertedItems.length,
      itemBuilder: (context, index, animation) {
        
        DonutModel currentDonut = widget.donuts![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 {
  DonutModel? donutInfo;
  DonutCard({ this.donutInfo });

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        Container(
          width: 150,
          padding: EdgeInsets.all(15),
          alignment: Alignment.bottomLeft,
          margin: EdgeInsets.only(left: 10, top: 80, right: 10, bottom: 20),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(20),
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(0.05), 
                blurRadius: 10, 
                offset: Offset(0.0, 4.0)
              )
            ]
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('${donutInfo!.name}', 
                style: TextStyle(
                  color: Utils.mainDark, 
                  fontWeight: FontWeight.bold, 
                  fontSize: 15
                )
              ),
              SizedBox(height: 10),
              Container(
                decoration: BoxDecoration(
                  color: Utils.mainColor,
                  borderRadius: BorderRadius.circular(20),
                ),
                padding: EdgeInsets.only(
                  left: 10, right: 10, top: 5, bottom: 5
                ),
                child: Text('\$${donutInfo!.price!.toStringAsFixed(2)}', 
                  style: TextStyle(
                    fontSize: 12, 
                    color: Colors.white, 
                    fontWeight: FontWeight.bold
                  )
                )
              )
            ]
          ),
        ),
        Align(
          alignment: Alignment.topCenter,
          child: Image.network(
            donutInfo!.imgUrl!, 
            width: 150, height: 150, 
            fit: BoxFit.contain
          ),
        )
      ]
    );
  }
}

class DonutBottomBarSelectionService extends ChangeNotifier {

  String? tabSelection = 'main';

  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 = [];

  DonutService() {
    selectedDonutType = filterBarItems.first.id;
    filteredDonutsByType(selectedDonutType!);
  }

  void filteredDonutsByType(String type) {
    selectedDonutType = type;
    filteredDonuts = Utils.donuts.where(
      (d) => d.type == selectedDonutType).toList();

    notifyListeners();
  }


}

class DonutFilterBarItem {
  String? id;
  String? label;

  DonutFilterBarItem({ this.id, this.label });
}

class DonutPage {
  String? imgUrl;
  String? logoImgUrl;
  
  DonutPage({ this.imgUrl, this.logoImgUrl });
}

class DonutModel {

  String? imgUrl;
  String? name;
  String? description;
  double? price;
  String? type;

  DonutModel({
    this.imgUrl,
    this.name,
    this.description,
    this.price,
    this.type
  });
}

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'
    )
  ];
}