What You'll Build in this Workshop:

Details Page

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

Let's check out a schematic view of what we'll be tackling in this codelab:

Details Page

We will tackle the layout in sections: the AppBar, then split the Scaffold body into two sections: top (where the image is) and bottom (where the rest of the content is).

Let's start!

We'll start by setting up the core page widget that will encapsulate the details page functionality and we'll call it DonutShopDetails. We'll start making it as a StatefulWidget since later on we'll add AnimationController and make use ticker mixins (SingleTickerProviderStateMixin) so create the custom widget class (using the 2-class approach for StatefulWidget widgets):

Details Page

Notice inside the DonutShopDetails page I'm returning a Scaffold to set up the foundation of my full-blown page.

Set up the App to always land on the DonutShopDetails (For Development Purposes only)

Go to the runApp method at the top, and in the routes, comment out the initial route ("/", the one that returns the SplashPage), and copy it, making its destination our newly created page DonutShopDetails, that way when we do hot reload, we end up on that page.

Details Page

With that in place, it's time to lay out the rest of the pieces, starting from the AppBar.

We'll pretty much use the same thing we used on pt.2 of this series, so we'll save time and just bring it over. We will only deal with this AppBar later when we add the DonutShoppingCartBadge much later.

In the DonutShopDetailsState class, set the Scaffold's *appBar property to be the AppBar we had on the DonutShopMain, as such:

Details Page

Run it on DartPad to see that we're off on a good start:

Details Page

That was easy! We have indeed started on the right foot! Let's keep moving!

In order to have a robust layout, a great foundation is key! We'll distribute the Scaffold's body layout as follows:

Details Page

Let's express the schematic view above into a tangible format.

Start by setting the root of the body as a Column widget. This Column will have two widgets laid out vertically: a Container (our top region), and an Expanded widget (our bottom region); add a Column as a placeholder child to the Expanded widget since it is a requirement for it.

Your code should look like this:

Details Page

That was the layout of the Scaffold's body. In the next section we will focus only on the Container (top region) of this DonutShopDetails page. Click Next to proceed.

Details Page

Let's tackle the top region in this details page.

We want to achieve an offset look on the donut image, so it looks a bit off to the side (on purpose!). In order to make something go off the screen, you can use a Stack widget as the parent, and a Positioned widget, setting either the left or right property to a negative number, forcing it to go off the screen.

Let's start.

In the DonutShopDetailsState class, create a new property called selectedDonut, type DonutModel. We will use it later when we bring the selected donut item from the previous page. For now we'll hardcoded from the static data in our Utils page.

Details Page

In the build method, initialize it with one of the donut models off of the Utils class, as follows:

Details Page

Great, now we're ready to move on.

Inside of the Container widget, we'll set it to be, not a fixed height, but a calculated value extracted from the system based on the height of the device in which this app is being rendered, therefore we'll use MediaQuery for that. We'll make the Container half the height of the screen, as follows:

Details Page

As the child of that container, add a Stack widget. Set its children property to an empty array by default:

Details Page

The reason why we made the Container the immediate parent of the Stack is because the Stack inherits the dimensions of the enclosing parent (in our case the Container).

Now, as the child of the Stack, add an Image.network widget (since the image is coming from a URL). Set the width to be a bit larger than the width of the device screen (1.25 should do); set the fit to BoxFit.contain, as shown below:

Details Page

Running it on DartPad now, at least it shows the image - but right in the middle of the Stack. We want to achieve the off-screen effect.

Details Page

Here comes Positioned widget to the rescue!

Wrap the Image.network widget inside a Positioned widget. To achieve the offset look, manipulate the properties top and right accordingly - I noticed that top: -40 and right: -120 worked well for me, so you should try these.

Your code should look like this after the change:

Details Page

And running it once again on DartPad, should give you the following output:

Details Page

Mission accomplished!!!!

If you're ready to move on to the next step, where we'll be tackling the bottom region, click Next.

Let's take a look at the schematic view of the bottom region below:

Details Page

The Expanded widget is where we'll concentrate our efforts this time. Again, we use Expanded since the framework will prioritize this region and make it occupy most of the real estate, after it allocates half of the screen to the top region as we shown earlier.

We've already laid out the Expanded widget with a Column inside, hinting that the layout will be vertical for the most part.

Let's start!

First things first, let's add some padding around the Column widget in order to give it some breathing room - 30px should do - as such:

Details Page

Proceed to initialize the Column widget with an empty array as its children property, and ensuring its children are left align (using CrossAxisAlignment.start):

Details Page

With that in place, let's add the first row of widgets that hold the name and the favorites button.

First Row: Name and Favorites

Let's add as the first child of the Column widget, a Row widget, also with its children aligned to the left, and initialized with an empty array as its children property:

Details Page

Let's focus on this Row widget for a minute. The first item in this Row is a Text widget that holds the name of the product. We want it to occupy most of the real estate in this row, so we know what to do: wrap it inside an Expanded widget.

Add the following code with the specs for the Text widget as follows:

Your code should look like this:

Details Page

Give it a go on DartPad and run what we have so far:

Details Page

Let's proceed with the rest. Let's add the favorite button to the right of the name. Add some breathing room between the name and the icon button using a SizedBox with a 50px width - to force the name Text widget width thus wrapping its contents a bit.

Details Page

Now add the IconButton - this will be used for later when we implement the favorites feature. For now, add it with the following specs:

Your code should look as follows:

Details Page

Take this for a spin on DartPad, this should be the output so far:

Details Page

Moving on - let's add some vertical spacing, thanks to the SizedBox widget. Add 10px of height right under the Row widget:

Details Page

Nice! Now let's tackle the price badge with a pill effect, which we'll accomplish by using a Container with the following specs:

Details Page

Now add the Text label that will hold the price, as a child of the Container we just created, also with the following specs:

Here's the complete code:

Details Page

Looking good so far on DartPad if you run it now:

Details Page

Next up, add yet more space via a SizedBox with a 20px height:

Details Page

Let's populate the description of our product. This next one is easy - just feed it into a Text widget using the selectedDonut.description property; we're ok with the default styling, font sizing and all that out of the box from a text widget.

Details Page

Quick run through DartPad as a sanity check, and then let's move on:

Details Page

So far so good. Let's do the bottom part of the details page, which is the button to add to the cart. We'll also add the functionality of this button later in the codelab where we implement the shopping cart capabilities.

For now let's build a custom button.

Creating a Custom Button using Core Widgets

Let's start with a plain ol' Container widget as the foundation of our simple custom button; add it as the last item of the Column widget inside the Expanded widget we placed earlierd:

Details Page

We'll make this Container look like a button, so let's start with the following specs:

Your code should look like this:

Details Page

As the child of the Container, going by the design above, we want to add an icon, some space and then the label "Add To Cart", so let's do just that. These items are laid out horizontally, so - yeah, you guessed it - we'll use a Row for that.

Add a Row as the immediate child of the Container, with its alignments centered on the row's main axis. Initialize its children with an empty array by default, as such:

Details Page

As mentioned before, we'll be adding 3 items: an icon, a bit of spacing and a label, so let's do just that.

Let's add the Icon widget, with the Icons.shopping_cart icon and assign the Utils.mainDark color to it:

Details Page

Give it some breathing room, so add the spacing using a SizedBox with a 20px width:

Details Page

And last - but not least! - the label as a Text widget, with the content "Add To Cart", adding a TextStyle with the color property set to Utils.mainDark:

Details Page

Testing it one more time in DartPad and see how's looking:

Details Page

Awesome! And now that our page is fully developed, let's hook it up to our DonutMainPage, that when upon the user tapping on one of the DonutCard widgets, the app will navigate to the details page, passing as context the model of the selected donut, allowing the DonutShopDetails page to display the information about the selected product appropriately.

Click Next to see how this is done.

We need to establish who will be triggering the navigation from the DonutShopMain page, store the context in question - the selected donut - in a common place, then provide that context to the requesting page - the details page. We'll use the DonutService again as the holding place for this functionality, in order to decouple it from either widget, that way they can share the information in a decoupled fashion.

Go to the DonutService class, and introduce a new property called selectedDonut, type DonutModel. Since at the beginning a user hasn't selected anything (only until they tap on the desired donut in the DonutList widget is that this property will be assigned a value), you can appropriately prefix it with the accesibility modifier called late:

Details Page

Let's now encapsulate the logic of persisting this value as well as handling the navigation to the desired route.

In the same DonutService, create a method called getSelectedDonut() which will serve as a getter method or wrapper to the selectedDonut property, as such:

Details Page

Add another method to then set the selected donut. 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, as follows:

Details Page

With this, we've ensured proper encapsulation of functionality that shouldn't be on a widget. Awesome!

Let's now proceed to set up the route and navigate to the details page.

Since we've developed the DonutShopDetails widget page, we can safely revert the changes we made at the root MaterialApp widget, where we temporarily pointed the page to the root named route ("/") and instead add a dedicated route for this page, so let's do just that.

Create a Dedicated Route for the DonutShopDetails

While undoing the change we made before so the root named route pointed to the details page, go ahead and add a new route, named appropriately "/details" and make it point to the DonutShopDetails widget page, as such:

Details Page

Now we'll be able to navigate to this page since it has its own route!

Let's add the code to capture the selection of the desired donut, plus performing the navigation.

Add tapping capabilities to the DonutCard

Upon the user tapping on one of the DonutCard widgets, we will capture the value, perform a navigation, and when the user is taken to the details page, the details page will efficiently pick up the stored value and display itself accordingly, See the workflow below - pretty much this is what we'll be accomplishing:

Details Page

Go to the DonutCard widget we created earlier.

Wrap the current Stack widget being returned by the build method inside a GestureDetector, as such:

Details Page

As before, we are using the GestureDetector to give this widget tapping capabilities.

On the onTap event available in the GestureDetector, attach a handler. Inside, obtain a reference to the DonutService using the Provider.of instance along with the provided Context, using the listen: false option, as follows:

Details Page

With the DonutService reference in hand, call the method onDonutSelected and supply the reference to a DonutModel available in this widget called donutInfo as follows:

Details Page

Now, all we have to do is ensure that the DonutShopDetails page picks up the value on the other end and bind it to its internal selectedDonut property.

In the DonutShopDetailsState class, inside its build method, do the same as in the DonutCard widget - pull a reference to the DonutService using the Provider.of method with the listen: false option.

Then, extract the selected donut by calling the method getSelectedDonut out of the DonutService reference, and assigning it to the local property selectedDonut. No other changes required!

Let's take this for a spin and confirm that the wiring is correct.

Details Page

Amazing work! With that we've concluded the core functionality of the details page in this app. If you want to go the extra mile, where we'll be adding animations to this page, you can go to the bonus step by clicking Next, otherwise you're good to go!

Congrats in making it this far! In this codelab, we accomplished the following:

In the next colab, we'll work on a simple shopping cart experience, while addressing more on state management, Consumer widgets and more!

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

This is just one of those little touches you give your application to make it stand out, and make it less static - totally optional though, but you'll learn how to apply explicit animations simple and quickly.

Details Page

Let's start!

Go to the DonutShopDetailsState page and let's start by adding a mixin class called SingleTickerProviderStateMixin - this is required when you have an AnimationController in your widget.

Details Page

Proceed to add two properties, one of type AnimationController and another one of type Animation<double>:

Details Page

Initialize both AnimationController and Animation by overriding the initState method in this StatefulWidget.

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.

Your code inside the initState method should look like this:

Details Page

Let's recap on what we did. 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).

Override the dispose method and invoke the dispose method on the animation controller created above:

Details Page

Now let's implement the rotation on the donut image, by using a RotationTransition - one of the available explicit animation widgets in the library. Locate the Image.network widget, inside the Positioned widget, and wrap the image inside the RotationTransition widget, passing as its turns property the rotationAnimation instance we created earlier, making this the direct child of the Positioned widget instead:

Details Page

And with that, you have a nice, slick, minimalist spinning animation - make sure not to make your users dizzy!!! Enjoy!

Details Page

Details Page

As an extra bonus, we'll implement Hero animation, which is a type of widget that allows you to animate a widget from one screen to another. It's simple to implement. Just mark the widgets at both ends of the transition as a Hero image using the same tag and you're done!

In our case, our source hero widget would be the donut image inside the DonutCard widget, and the destination hero widget will be the image in the DonutShopDetails, giving the illusion of the same image sliding from one screen to the other, as shown above.

Let's start!

Go to the DonutCard widget, and locate the Image.network inside the Align widget. Wrap the Image widget inside a Hero widget, and set its tag to be the product name (donutInfo.name)

Details Page

With that in place, now to to the DonutShopDetails page, where our destination hero should be set.

Locate the RotationTransition widget that's currently wrapping the Image.network widget. Instead of wrapping the image directly, we are wrapping the whole rotation so as the Hero to apply to the animation as well, therefore place the Hero as a direct child of Positioned, wrapping RotationTransition as well. Set its tag to be the selectedDonut.name which matches in tag with the DonutCard's donutInfo.name:

Details Page

Running what we have so far in DartPad, you should see the full animation sequence, starting from the landing page, tapping on a card, going to the details page. Looks flawless!!!

Thanks for making it this far. If you completed both of the bonus labs, now you know how to add that extra special ingredient to your apps to make them stand out of the rest, with very little effort!

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(),
          '/details': (context) => DonutShopDetails(),
        }
      )
    )
  ); 
}

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 GestureDetector(
      onTap: () {
        var donutService = Provider.of<DonutService>(context, listen: false);
        donutService.onDonutSelected(donutInfo!);
      },
      child: 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: Hero(
              tag: donutInfo!.name!,
              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 = [];


  late DonutModel selectedDonut;

  DonutModel getSelecteDonut() {
    return selectedDonut;
  }

  void onDonutSelected(DonutModel donut) {
    selectedDonut = donut;
    Utils.mainAppNav.currentState!.pushNamed('/details');
  }

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

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

    notifyListeners();
  }
}

class DonutShopDetails extends StatefulWidget {
  @override
  State<DonutShopDetails> createState() => _DonutShopDetailsState();
}

class _DonutShopDetailsState extends State<DonutShopDetails> 
    with SingleTickerProviderStateMixin{

  DonutModel? selectedDonut;
  AnimationController? controller;
  Animation<double>? rotationAnimation;

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

    controller = AnimationController(
      duration: const Duration(
        seconds: 20), 
        vsync: this)..repeat();

    rotationAnimation = Tween<double>(begin: 0, end: 1)
    .animate(CurvedAnimation(
      parent: controller!, curve: Curves.linear));
  }

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

  @override
  Widget build(BuildContext context) {
    DonutService donutService = Provider.of<DonutService>(context, listen: false);
    selectedDonut = donutService.getSelecteDonut();

    return Scaffold(
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Utils.mainDark),
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: SizedBox(
          width: 120,
          child: Image.network(Utils.donutLogoRedText)
        )
      ),
      body: Column(
        children: [
          Container(
            height: MediaQuery.of(context).size.height / 2,
            child: Stack(
              clipBehavior: Clip.none,
              children: [
                Positioned(
                  top: -40,
                  right: -120,
                  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),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Expanded(
                        child: Text('${selectedDonut!.name!}',
                          style: TextStyle(color: Utils.mainDark, 
                          fontSize: 30, 
                          fontWeight: FontWeight.bold)    
                        )
                      ),
                      SizedBox(width: 50),
                      IconButton(
                        icon: Icon(Icons.favorite_outline),
                        color: Utils.mainDark,
                        onPressed: () {}
                      )
                    ]
                  ),
                  SizedBox(height: 10),
                  Container(
                    padding: EdgeInsets.only(top: 10, bottom: 10, left: 20, right: 20),
                    decoration: BoxDecoration(
                      color: Utils.mainDark,
                      borderRadius: BorderRadius.circular(20),
                    ),
                    child: Text('\$${selectedDonut!.price!.toStringAsFixed(2)}', 
                      style: TextStyle(color: Colors.white)
                    ),
                  ),
                  SizedBox(height: 20),
                  Text('${selectedDonut!.description!}'),
                  Container(
                    margin: EdgeInsets.only(top: 20),
                    alignment: Alignment.center,
                    padding: EdgeInsets.only(left: 20, right: 20, top: 10, bottom: 10),
                    decoration: BoxDecoration(
                      color: Utils.mainDark.withOpacity(0.1),
                      borderRadius: BorderRadius.circular(50)
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(Icons.shopping_cart, color: Utils.mainDark),
                        SizedBox(width: 20),
                        Text('Add To Cart', style: TextStyle(color: Utils.mainDark)),
                      ]
                    )
                  )
                ],
              ),
            )
          )
        ]
      )
    );
  }
}

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