What You'll Build in this Codelab:

Details Page

The following illustration shows a schematic view of the widget composition we'll accomplish while building the layout for our landing page widget:

Details Page Schematics

We'll further break down each of the widgets depicted above for further breakdown and composition.

Since we'll be developing this page further, we want our application to skip the splash screen while we work on it, so we will temporarily point the main application widget to this page.

In the main method, comment out the SplashPage value assigned to the home property of the MaterialApp and add the DetailsPage instead, as shown below:

main home DetailsPage

Let's proceed.

Right under the MountsApp class we created earlier, add a new class called DetailsPage that extends StatelessWidget.

As always, override its build method, and for the common exercise to test that things are working and hooked up, return a Scaffold widget, and set its body property to a Center widget wrapping a Text widget, with the temporary content "Details Page.

DetailsPage

The boilerplate code for this page is available for copy / paste below:


// Details Page 
class DetailsPage extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Details Page')      
      )
    );
  }
}

If you run things as they are right now (by hitting Run on DartPad), things should look as follows:

DetailsPage

Building the Details Page widget structure

Let's kick things off by adding a background color to the Scaffold widget returned by the DetailsPage widget's build method:

DetailsPage

Replace the contents of the body property by a Column widget. We'll split the content right down the middle (we'll add two regions - a top and a bottom), so in order to ensure each of the column's children get the same space, we'll wrap each child inside an Expanded widget.

Let's do a quick test and add two Expanded widgets, each one wrapping a Text widget; I'll have the top one say "Details Top" and the bottom one say "Details Bottom":

DetailsPage

Things should look as follows if we do a quick run on DartPad:

DetailsPage

And that's how you implement regions in your app with equally distributed space among them. The top region will be for the image, some text and the AppBar, and the bottom region will be for the rest of the content.

In the next section, we'll focus only on the top region. Proceed by clicking Next.

This is a schematic view of the top region of the DetailsPage widget that we'll be developing:

DetailsPage

This is the cross-section illustration of the same widget, how I usually visualize when using a layered approach (i.e. using a Stack with children) which is what we'll do for this instance.

DetailsPage

Let's proceed!

Getting a model for mocking the data on this page

Since we will be developing this page in isolation from the rest of the app for now, we need to have a way to bring the data that will hydrate this page. We'll fetch one of the items from the mountItems collection we created in a previous codelab and store that value within this widget (temporarily).

In the build method of the DetailsPage widget, create a local variable called selectedItem, and assign to it the mount object at index 0.

DetailsPage

Let's build the structure.

Replace the contents of the top Expanded widget (the Text widget) by a Stack widget. Initialize it by setting its chidren property with an empty array.

DetailsPage

Let's add the background image. We'll use a Container widget, with the following specs:

Here's the code for that Container, to save you some typing:


// top Container widget

Container(
  padding: EdgeInsets.all(30),
  decoration: BoxDecoration(
    image: DecorationImage(
      image: NetworkImage(selectedItem.path),
      fit: BoxFit.cover
    )
  )
)

Your code should look as follows:

DetailsPage

Hitting Run at this point on DartPad will yield the following result:

DetailsPage

Notice how the Stack, which wraps the image, and itself is wrapped by an Expanded widget, takes up the whole top half of the column, as expected.

Let's continue overlapping the rest of the children.

We'll add a bit of gradient at the bottom of the Stack so when we place the text content, we can read it better and provide better contrast. We want the gradient to start at the bottom and stops at the center of the Stack with a smooth transition from opaque black to transparent.

Add a Positioned widget as the second child of the Stack, using the fill constructor (which makes it stretch and fill the whole Stack space since by default it has its left, top, right and bottom set to 0.0):

DetailsPage

The child of this Positioned widget will be a Container, which will supply the gradient effect using its decoration property.

For the decoration property, use the BoxDecoration object's gradient property. Set it to be a LinearGradient object, where you supply the following:

Add the Container with the LinearGradient as decoration inside the Positioned.fill() widget; code provided below:


// add this code inside the Positioned.fill(), as a child:
child: Container(
  padding: EdgeInsets.all(30),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [
        Colors.transparent,
        Colors.black.withOpacity(0.7)
      ],
      begin: Alignment.center, end: Alignment.bottomCenter
    )
  )
)

Your code should look like this afterwards:

DetailsPage

Now, lets add the text on top of the gradient.

The text content as seen in the design, are positioned at the bottom left corner of the Stack, so we'll do just that.

First, add a Positioned widget (below the Positioned.fill widget added earlier), aligned 30px from the bottom and 30px from the left of the Stack, and as its child, add a Column widget:

DetailsPage

The column's children will be two Text widgets, left aligned on the column.

First Text widget will hold the name of the selected item (from the local variable selectedItem.name), with font size of 20px, color white, and font weight of bold.

Second Text widget will hold the location of the selected item (from selectedItem.location), with font size of 20px, and color white.

Replace the Column widget above by the code provided below:


// replace Column widget:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text(selectedItem.name,
      style: TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold)    
    ),
    Text(selectedItem.location,
      style: TextStyle(color: Colors.white, fontSize: 20)    
    ),
  ]
)

Your code should look like this now:

DetailsPage

Testing it on DartPad, it should look like this so far:

DetailsPage

Let's continue by adding the AppBar widget.

Yes - you might be wondering "why are we adding the AppBar as a child to the Stack as opposed to where it usually goes - as the appBar of the Scaffold widget?". Well, adding it to the Scaffold will also add it at the top of the screen, but will devote a fixed space at the top of the screen and won't allow items to overlap it or show through.

Add an AppBar widget as a child of the Stack widget, with the following specs:

DetailsPage

As in the AppBar widget of the MountsApp page widget, we'll set its title property to be an Icon widget, wrapped inside a Center widget, as follows:

DetailsPage

We'll add a mocked action item to the AppBar widget (no functionality, just a placeholder), just to show you how you can utilize the AppBar widget's actions property.

We'll add a Container widget with 10px of right margin, wrapping an Icon widget, color white with a size of 30px:

DetailsPage

Last, but not least, let's clip the bottom edges of the Stack as a whole and give it round edges, in order to match our design.

Wrap the whole Stack widget inside a ClipRRect (as the name implies, it will clip its contents in a "rounded rectangle"). Clip only the bottom left and bottom right corners by using the BorderRadius.only and setting the bottomLeft and bottomRight properties respectively:

DetailsPage

Previewing on DartPad after hitting Run should yield the following result:

DetailsPage

Looking good! This was a great step, because you learned the following:

And with that, we're done with the top region of the DetailsPage widget!.

Click Next when you're ready for the next step.

Let's now focus on the bottom region of the DetailsPage widget, where the information of the selected mount is displayed.

DetailsPage

The following illustration is the schematic view of the bottom region, and shows the widget composition and how we will tackle it.

DetailsPage

Working on the layout

Let's start!

Replace the existing content of the second Expanded widget (the Text widget) by a Column widget; the core layout will be column, laying out its children vertically. Initialize it by setting its children property to an empty array:

DetailsPage

Now, let's add child widgets to this column.

Creating the Details Rating Bar

The first child will be dealing with is this ratings bar which contains rating, price and open hours information. We'll make it into its own widget and we'll call it DetailsRatingBar for simplicity.

DetailsPage

Add a placeholder widget as the first child of this column:

DetailsPage

Create a class under the WIDGETS section (if you didn't create this section, you can add it at the end of the file) and call it DetailsRatingBar as well. Override its build method and return a Container widget with a Text widget as a child; add the content "Details Rating Bar" to the Text widget:

Boilerplate code for this widget below, for copy / pasting:


// details rating bar widget

class DetailsRatingBar extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('Details Rating Bar')
    );
  }
}

If you hit Run on DartPad, you should see the following:

DetailsPage

Creating a quick model to feed this widget with data

Previously, we created Dart model classes and populated them with mocked data to generate layouts with semi-realistic data. This case won't be the exception but in this case we'll create a simple key-value pair list of dummy data to drive the values that hydrate this component.

Inside the DetailsRatingBar class, on top of the build method, create a quick dictionary with key-value pairs; each entry's key will be the label, and the value - well, the value:

DetailsPage

Now let's develop the structure within this widget.

Replace the content of the Container widget within the DetailsRatingBar by a Row widget. Add a padding of 10px all around to the Container as well:

DetailsPage

We want the Row's children to be spaced out evenly, so set its mainAxisAlignment property to MainAxisAlignment.spaceEvenly:

DetailsPage

Let's populate the children of the Row widget based on the key-value pair dictionary we created above, and loop through its entries.

Use the List.generate constructor to facilitate things; use the sampleRatingData.values.length as the size of the list to loop against, and in the callback, return an empty Container widget as a placeholder.

As the Row's children property, use the code provided below - copy / paste it:


// list generate utility method - add the following code inside the Row widget created above

children: List.generate(
  sampleRatingData.entries.length,
  (index) => Container()
)

Your code should look as follows:

DetailsPage

Now, for each of the entries of the sampleRatingData, we'll generate a Container widget. Let's use the Container widget returned by this callback method and continue building on its structure.

To this container, add padding of 20px all around, and margin of 10px all around as well:

DetailsPage

We want to add border all around this Container, as well as rounded corners, via its decoration property:

You can grab the code below for adding both decoration and borderRadius to the Container widget:


// inside the Container...

decoration: BoxDecoration(
  border: Border.all(color: Colors.grey.withOpacity(0.2),
  width: 2
),
borderRadius: BorderRadius.circular(20)

Your code should look like this:

DetailsPage

As a child of this container, we'll add a Column widget - this column will hold the two Text widgets required to display the label (top) and value (bottom).

DetailsPage

Add the top Text widget and feed the current entry's key, color black, and bold.

The bottom Text widget will get the current entry's value, with color of mainColor, font size of 15px and font weight of bold:

Below the code for the Column's children - the two Text widgets for key and value:


// replace the Column widget's children with the code below:

children: [
  Text(sampleRatingData.entries.elementAt(index).key,
    style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)    
  ),
  Text(sampleRatingData.entries.elementAt(index).value,
    style: TextStyle(
      color: mainColor, 
      fontWeight: FontWeight.bold,
      fontSize: 15
    )    
  ),
]

Your code should look like this:

DetailsPage

If you run the code as it stands right now, by hitting Run on DartPad, you should get the following output in the preview panel:

DetailsPage

Neat! This was a quick component, fed using some quick mock data generated using key-value pairs from a dictionary, and we proved how easy it is to generate layouts driven by collections with mock data.

Let's dissect the following structure, which is what we'll be building next:

DetailsPage

Moving down the widget tree that we have in the design, now it's the turn of the title and description widgets, which will be accomplished by two Text widgets laid out as a column. We want these two widgets to occupy most of the real estate of the bottom half region of this page, so we'll wrap their Column widget inside an Expanded widget.

Back in the DetailsPage widget, start by adding the Expanded widget, right under the DetailsRatingBar widget we just created:

DetailsPage

Now, let's focus on building the structure for this section.

For this Expanded widget, add its one and only widget, a Column widget - the one that will hold the title and description:

DetailsPage

Let's add the first Text widget, whose content will be fed from the local variable we set up earlier (selectedItem) and its property called name. Set the following properties of the Text widget as follows:

While displaying the selectedItem.name content, use variable interpolation and add the text "About" in front of whatever comes out of the name property (i.e. ‘About ${selectedItem.name}').

Below the code for the Text widget, added as the first child of the Column's children property:


// Text widget with the name title

children: [
  Text('About ${selectedItem.name}',
   textAlign: TextAlign.left,
   style: TextStyle(
     color: Colors.black,
     fontSize: 20,
     fontWeight: FontWeight.bold
   )
  )
]

Your code should look as follows:

DetailsPage

To give it some breathing room, wrap the Text widget inside a Padding widget, with left, right and bottom padding of 20px:

DetailsPage

For the description content, add another Text widget, font size of 12px, but this time, wrap it inside another Padding widget, to match the title spacing above (left, right, and bottom padding of 20px):

DetailsPage

If you take it for a spin on DartPad, you'll see now both the title and description, with proper padding around them, under the DetailsRatingBar widget, as we expected:

DetailsPage

Sweet! We're done with the content, and now we'll proceed with the bottommost portion of the DetailsPage widget.

To wrap up the layout of the DetailsPage widget we'll tackle the button panel at the bottom of our design:

DetailsPage

Dissecting this same widget, we can check its widget composition and appreciate clearly how it is composed, as shown on the following illustration:

DetailsPage

Let's get to work!

Creating the placeholder widget for the DetailsBottomActions widget

Back on the DetailsPage widget, under the Expanded widget we created earlier to hold the title and description, let's add the placeholder widget instance; let's call it DetailsBottomActions:

DetailsPage

In the WIDGETS section (if you created, otherwise, anywhere at the bottom of this file), create a new class called DetailsBottomActions (just like our placeholder above).

Make the class extend StatelessWidget, and as customary, override its build method, returning a temporary Text widget, just to make sure the placement is fine:

Below the boilerplate code for the DetailsBottomActions Widget, in case you want to save some keystrokes:


// DetailsBottomActions Widget

class DetailsBottomActions extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Text('Bottom Actions');
  }
}

Doing a dry-run on the code that we just wrote by hitting Run on DartPad, we see the Text widget at the bottom, confirming its hooked up:

DetailsPage

Working on the Layout

Let's start by replacing the dummy Text widget we placed for testing, with a Padding widget, since we want our bottom actions panel to match on spacing around itself with the other components above. Add left, right and bottom padding, 20px:

DetailsPage

Recall from the image above, we have two buttons, laid out horizontally. For that, we need to set a Row widget as the child of this Padding widget; initialize its children property with an empty array:

DetailsPage

Now, take a closer look at our bottom actions panel. The left button (Book Now) is wider than the button on the right (button with the tag icon). We'll do the same approach as in previous occasions: the widget we want it to take most of the space inside a Row or Column, we wrap it inside an Expanded panel.

Right away, let's set the stage for these two buttons; inside the Row widget, add two children: an Expanded widget with a Container widget as its child, and right underneath it, an empty Container, as follows:

DetailsPage

Creating a Custom Button using core and Material widgets

For the Book Now button, we won't use one of the buttons that come with the Material library - we'll create own own from scratch, even with an ink well effect and all.

Let's proceed.

Replace the empty Container widget we added inside the Expanded widget (we'll be adding it again down the hierarchy) by a Material widget. This widget is very versatile and is at the core of the material design methaphor.

We'll use it for clipping the edges of its widget subtree, by setting its borderRadius property to BorderRadius.circular(15).

Assign a color to the Material widget as well, so set its color property to our global mainColor defined earlier:

If you take it for a spin right now, you won't see anything - because this widget has no child to display at the moment.

DetailsPage

Another widget that is instrumental in bringing cool touch effects to your buttons is the InkWell widget, which is a rectangular area (part of the Material Design library) that responds to touch. You don't only apply it to buttons, you can apply it to other touch-capable surfaces.

Add an InkWell widget as the child of the Material widget. Set the following properties on the InkWell as follows:

Below the code for the InkWell widget, ready to be added as a child of the Material widget:


// add as child of the Material widget:

child: InkWell(
  highlightColor: Colors.white.withOpacity(0.2),
  splashColor: Colors.white.withOpacity(0.2),
  onTap: () {}
)

Your code should look as follows:

DetailsPage

After all that prep work to be able to have a custom button, let's add what the actual content for the button will be.

Let's add a Container widget, with padding of 21px all around. As its child. let's add a Text widget with the text of "Book Now". Set its textAlign property to TextAlign.center, and set its color to white via the TextStyle color properly. You may have noticed I didn't add a color to the Container widget. I don't have to, because the Material widget, since it's the topmost parent, is the one that holds the background color - the Container is just a mere container for holding the Text widget and adding the padding.

If you do the above steps, your code should look as follows:

DetailsPage

Thanks for being patient! Now if you hit Run on DartPad, you definitely see something... The fully created custom button! Test the experience when you hover, and press on any spot within the button, and you'll see the ink well in effect. Neat!

DetailsPage

Let's wrap up this widget by adding the remaining button on the right side of this panel.

Let's utilize that empty Container widget under the Expanded widget we created earlier. On this same container, set some of its properties with the following values:

Below the code for this custom Container, to avoid extra typing:


// Container for the button on the right side

Container(
  margin: EdgeInsets.only(left: 10),
  padding: EdgeInsets.all(15),
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(15),
    color: Colors.white,
    border: Border.all(color: mainColor, width: 2)
  )
)

Follow the specs and code above and your code should look as follows:

DetailsPage

Now, all we have to do is add a child to this Container widget - the tag icon. We'll use an Icon widget (Icons.turned_in_not), color mainColor and 25px in size:

DetailsPage

If you run the code in DartPad now, you will see the bottom actions bar fully in place, matching our design:

DetailsPage

Feel proud of what you've built so far! With no special widgets, no imported libraries other than the Material, and some core Flutter widgets, we were able to build pretty cool interfaces in a short amount of time. Kudos to you!

Click Next so we can wrap up this workshop by bringing it all together.

Throughout this workshop, we focused on building layouts and structures in Flutter using core widgets on top of the Material library with ease and speed. It allowed us to build three screens: a SplashPage widget screent that loads initially, and after a duration, shows a landing page (MountsApp). Now we just finished developing a DetailsPage widget that will show the details of any selected item in the MountsApp page widget.

DetailsPage

Wait, we haven't done that yet! We need to add the capability of tapping / clicking on one of the items in the AppMountListView, which should then navigate us to the DetailsPage and show us the details of that selected mount.

How will we accomplish that? We'll do it in a simple manner: we'll pass the index of the selected item in the list view to the DetailsPage via its constructor, and then on the other end, we grab the model that corresponds to that index.

Edit the DetailsPage constructor to take an index

Navigate to the body of the DetailsPage class. Create a property called mount, type *MountModel. Create a constructor that takes the same named parameter as such:

DetailsPage

Now, remember the local variable we created inside the DetailsPage's build method (selectedItem). We have it hard-coded to always pull the item at index 0 from the mountItems collection. To minimize the amount of refactoring, just change it from pulling the first value out of the mountItems collection to just assigning the injected model to selectedItem, as such:

DetailsPage

Great. Things are hooked up on one end. Now, let's go to the AppMountListView widget. Locate the ListView.builder callback method - itemBuilder.

In the returning Container widget, refactor it by wrapping this container inside a GestureDetector - a very handy widget for detecting gestures. In our case we want to intercept a tap on one of the ListView container items:

DetailsPage

Let's tap into the onTap event available in the GestureDetector widget, since we want to capture tap events on each item. In this event, perform a navigation to our DetailsPage widget, passing the currentMount value corresponding to the current iteration.

Below the code for the onTap event handler, to avoid extra typing:


// add this inside the GestureDetector, as its onTap event:

onTap: () {
  Navigator.of(context).push(
    MaterialPageRoute(builder: (context) => DetailsPage(currentMount))
  );
}

DetailsPage

As a final step, in the main method at the top of the file, reset the home property in the MaterialApp widget back to pointing to the SplashPage widget, so we can test the whole workflow:

DetailsPage

Running it one last time by hitting Run on DartPad, and it will produce the full-fledged app! While taking it for a spin, ensure that the following are taking place:

Congrats on completing the three Flutter codelabs where we touch on the following:

Hope you've enjoyed these codelabs as much as I had putting them together.

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

Thanks for joining me on this Flutter journey! Cheers!!!!

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

//-----GLOBAL VARIABLES-----
final Color mainColor = Color(0xFFFF5656);

final List<MountModel> mountItems = [
  MountModel(
      path:
          'https://sa.kapamilya.com/absnews/abscbnnews/media/2021/afp/01/17/20210116-mt-semeru-indonesia-ash-afp-s.jpg',
      name: 'Mount Semeru',
      description:
          'Semeru, or Mount Semeru, is an active volcano in East Java, Indonesia. It is located in the subduction zone, where the Indo-Australia plate subducts under the Eurasia plate. It is the highest mountain on the island of Java.',
      location: 'East Java'),
  MountModel(
      path:
          'https://media-cdn.tripadvisor.com/media/photo-s/04/a5/6f/ce/dsc-5622jpg.jpg',
      name: 'Mount Merbaru',
      description:
          'Mount Merbabu is a dormant stratovolcano in Central Java province on the Indonesian island of Java. The name Merbabu could be loosely translated as Mountain of Ash from the Javanese combined words; Meru means mountain and awu or abu means ash.',
      location: 'Central Java'),
  MountModel(
      path: 'https://cdn.dlmag.com/wp-content/uploads/2019/07/maunaloa1.jpg',
      name: 'Mauna Loa',
      description:
          'Mauna Loa is one of five volcanoes that form the Island of Hawaii in the U.S. state of Hawai in the Pacific Ocean. The largest subaerial volcano in both mass and volume, Mauna Loa has historically been considered the largest volcano on Earth, dwarfed only by Tamu Massif.',
      location: 'Hawaii'),
  MountModel(
      path:
          'https://cdn.images.express.co.uk/img/dynamic/78/590x/mount-vesuvius-1100807.jpg',
      name: 'Mount Vesuvius',
      description:
          'Mount Vesuvius is a somma-stratovolcano located on the Gulf of Naples in Campania, Italy, about 9 km east of Naples and a short distance from the shore. It is one of several volcanoes which form the Campanian volcanic arc.',
      location: 'Italy'),
  MountModel(
      path:
          'https://upload.wikimedia.org/wikipedia/commons/0/04/PopoAmeca2zoom.jpg',
      name: 'Mount Popocatépetl',
      description:
          'Popocatépetl is an active stratovolcano located in the states of Puebla, Morelos, and Mexico in central Mexico. It lies in the eastern half of the Trans-Mexican volcanic belt. At 5,426 m it is the second highest peak in Mexico, after Citlaltépetl at 5,636 m.',
      location: 'Mexico')
];

final List<CategoryModel> categories = [
  CategoryModel(category: 'Mountain', icon: Icons.terrain),
  CategoryModel(category: 'Forest', icon: Icons.park),
  CategoryModel(category: 'Beach', icon: Icons.beach_access),
  CategoryModel(category: 'Hiking', icon: Icons.directions_walk)
];

//-----MAIN METHOD-----
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false, 
      home: SplashPage()
    )
  );
}

//-----PAGES-----
// splash page
class SplashPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    Future.delayed(const Duration(seconds: 2), () {
      Navigator.of(context)
          .push(MaterialPageRoute(builder: (context) => MountsApp()));
    });

    return Container(
      color: mainColor,
      child: Stack(
        children: [
          Align(
            alignment: Alignment.center,
            child: Icon(Icons.terrain, color: Colors.white, size: 90),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              margin: EdgeInsets.only(bottom: 80),
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
              )
            )
          )
        ],
      )
    );
  }
}

// landing page
class MountsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        elevation: 0,
        backgroundColor: Colors.transparent,
        title: Center(
          child: Icon(
            Icons.terrain, 
            color: mainColor, 
            size: 40
          )
        ),
        actions: [
          SizedBox(width: 40, height: 40)
        ],
        iconTheme: IconThemeData(color: mainColor)
      ),
      drawer: Drawer(
        child: Container(
          padding: EdgeInsets.all(30),
          color: mainColor,
          alignment: Alignment.bottomLeft,
          child: Icon(Icons.terrain, color: Colors.white, size: 80)
        )
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          AppHeader(),
          AppSearch(),
          Expanded(
            child: AppMountListView()
          ),
          AppCategoryList(),
          AppBottomBar()
        ],
      )
    );
  }
}

// details page
class DetailsPage extends StatelessWidget {

  MountModel mount;

  DetailsPage(this.mount);

  @override
  Widget build(BuildContext context) {
    var selectedItem = mount;

    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          Expanded(
            child: ClipRRect(
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(40),
                bottomRight: Radius.circular(40)
              ),
              child: Stack(
                children: [
                  Container(
                    padding: EdgeInsets.all(30),
                    decoration: BoxDecoration(
                      image: DecorationImage(
                          image: NetworkImage(selectedItem.path),
                          fit: BoxFit.cover
                        )
                      ),
                  ),
                  Positioned.fill(
                    child: Container(
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [
                            Colors.transparent,
                            Colors.black.withOpacity(0.7)
                          ],
                          begin: Alignment.center, end: Alignment.bottomCenter
                        )
                      )
                    )
                  ),
                  Positioned(
                    bottom: 30, left: 30, right: 0,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(selectedItem.name,
                          style: TextStyle(
                            color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold
                          )
                        ),
                        Text(selectedItem.location,
                          style: TextStyle(color: Colors.white, fontSize: 20)
                        ),
                      ]
                    )
                  ),
                  AppBar(
                    elevation: 0,
                    backgroundColor: Colors.transparent,
                    iconTheme: IconThemeData(color: Colors.white),
                    title: Center(
                      child: Icon(Icons.terrain, color: Colors.white, size: 40)
                    ),
                    actions: [
                      Container(
                        margin: EdgeInsets.only(right: 10),
                        child: Icon(Icons.pending,color: Colors.white, size: 30)
                      )
                    ]
                  )
                ],
              )
            )
          ),
          Expanded(
            child: Column(
              children: [
                DetailsRatingBar(),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
                        child: Text('About ${selectedItem.name}', 
                          textAlign: TextAlign.left,
                          style: TextStyle(
                            color: Colors.black,
                            fontSize: 20,
                            fontWeight: FontWeight.bold
                          )
                        )
                      ),
                      Padding(
                        padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
                        child: Text('${selectedItem.description}', style: TextStyle(fontSize: 12))
                      )
                    ],
                  )
                ),
                DetailsBottomActions()
              ],
            )
          )
        ],
      )
    );
  }
}

//-----WIDGETS-----

class AppHeader extends StatelessWidget {  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(left: 30, top: 30, right: 30),
      child: Row(
        children: [
          ClipOval(
            child: Image.network(
              'https://avatars.githubusercontent.com/u/5081804?v=4',
              width: 50,
              height: 50,
              fit: BoxFit.cover
            ),
          ),
          SizedBox(width: 20),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start, 
            children: [
              Text('Hello, Roman',
                style: TextStyle(
                    color: Colors.black, 
                    fontWeight: FontWeight.bold
                )
              ),
              Text('Good Morning',
                style: TextStyle(
                  color: mainColor, 
                  fontSize: 12
                )
              )
            ]
          )
        ],
      )
    );
  }
}

class AppSearch extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(30),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Discover',
            style: TextStyle(
              fontWeight: FontWeight.w900, 
              fontSize: 25
            )
          ),
          SizedBox(height: 20),
          Row(
            children: [
              Expanded(
                child: Container(
                  height: 50,
                  padding: EdgeInsets.all(10),
                  decoration: BoxDecoration(
                    color: Colors.grey[200],
                    borderRadius: BorderRadius.circular(10)
                  ),
                  child: Row(children: [
                      Icon(Icons.search, color: Colors.grey),
                      SizedBox(width: 10),
                      Text('Search', style: TextStyle(color: Colors.grey))
                    ]
                  )
                ),
              ),
              Container(
                margin: EdgeInsets.only(left: 10),
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                  color: mainColor,
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Icon(Icons.tune, color: Colors.white)
              )
            ],
          )
        ],
      )
    );
  }
}

class AppMountListView extends StatelessWidget {

  @override 
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: mountItems.length,
        itemBuilder: (context, index) {
          MountModel currentMount = mountItems[index];

          return GestureDetector(
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) => DetailsPage(currentMount))
              );
            },
            child: Container(
              alignment: Alignment.bottomLeft,
              padding: EdgeInsets.all(20),
              margin: EdgeInsets.all(10),
              width: 150,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(15),
                image: DecorationImage(
                  image: NetworkImage(currentMount.path),
                  fit: BoxFit.cover
                )
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(currentMount.name,
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold
                    )
                  ),
                  Text(currentMount.location,
                    style: TextStyle(
                      color: Colors.white
                    )
                  )
                ]
              )
            ),
          );
        }
      )
    );
  }
}

class AppCategoryList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Container(
            padding: EdgeInsets.all(20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('Category',
                  style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)
                ),
                Text('See More',
                  style: TextStyle(color: mainColor,fontSize: 12, fontWeight: FontWeight.bold
                  )
                ),
              ]
            )
          ),
          Container(
            height: 100,
            margin: EdgeInsets.only(left: 10),
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: categories.length,
              itemBuilder: (context, index) {

                CategoryModel currentCategory = categories[index];

                return Container(
                  width: 100,
                  margin: EdgeInsets.only(top: 10, right: 10),
                  padding: EdgeInsets.all(10),
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.grey.withOpacity(0.2), width: 2),
                    borderRadius: BorderRadius.circular(10)
                  ),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(currentCategory.icon, color: mainColor),
                      Text(currentCategory.category,
                        style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold))
                    ]
                  )
                );
              }
            )
          ),
        ],
      )
    );
  }
}

class AppBottomBar extends StatefulWidget {
  @override
  AppBottomBarState createState() => AppBottomBarState();
}

class AppBottomBarState extends State<AppBottomBar> {

  List<AppBottomBarItem> barItems = [
    AppBottomBarItem(icon: Icons.home, label: 'Home', isSelected: true),
    AppBottomBarItem(icon: Icons.explore, label: 'Explore', isSelected: false),
    AppBottomBarItem(icon: Icons.turned_in_not, label: 'Tag', isSelected: false),
    AppBottomBarItem(icon: Icons.person_outline,label: 'Profile', isSelected: false)
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 20),
      padding: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 10,
            offset: Offset.zero
          )
        ]
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: 
        List.generate(barItems.length, (index) {
            AppBottomBarItem currentBarItem = barItems[index];

            Widget barItemWidget;

            if (currentBarItem.isSelected) {
              barItemWidget = Container(
                padding: EdgeInsets.only(left: 15, top: 5, bottom: 5, right: 15),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(20),
                  color: mainColor
                ),
                child: Row(
                  children: [
                    Icon(currentBarItem.icon, color: Colors.white),
                    SizedBox(width: 5),
                    Text(currentBarItem.label, style: TextStyle(color: Colors.white))
                  ]
                )
              );
            }
            else {
              barItemWidget = IconButton(
                icon: Icon(currentBarItem.icon, color: Colors.grey),
                onPressed: () {
                  setState(() {
                    barItems.forEach((AppBottomBarItem item) {
                      item.isSelected = item == currentBarItem;
                    });
                  });
                }
              );
            }

            return barItemWidget;
          }
        )
      )
    );
  }
}

class DetailsRatingBar extends StatelessWidget {

  var sampleRatingData = {
    'Rating': '4.6',
    'Price': '\$12',
    'Open': '24hrs'
  };
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: List.generate(
          sampleRatingData.entries.length,
          (index) => Container(
              padding: EdgeInsets.all(20),
              margin: EdgeInsets.all(10),
              decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey.withOpacity(0.2), width: 2
                ),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Column(
                children: [
                  Text(sampleRatingData.entries.elementAt(index).key,
                    style: TextStyle(color: Colors.black,fontWeight: FontWeight.bold)
                  ),
                  Text(sampleRatingData.entries.elementAt(index).value, style: TextStyle(
                      color: mainColor,
                      fontWeight: FontWeight.bold,
                      fontSize: 15
                    )
                  )
                ]
              )
          )
        )
      )
    );
  }
}

class DetailsBottomActions extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
      child: Row(
        children: [
          Expanded(
            child: Material(
              borderRadius: BorderRadius.circular(15),
              color: mainColor,
              child: InkWell(
                highlightColor: Colors.white.withOpacity(0.2),
                splashColor:Colors.white.withOpacity(0.2),
                onTap: () {},
                child: Container(
                  padding: EdgeInsets.all(21),
                  child: Text('Book Now', textAlign: TextAlign.center,
                      style: TextStyle(color: Colors.white)
                  )
                ),
              )
            )
          ),
          Container(
            margin: EdgeInsets.only(left: 10),
            padding: EdgeInsets.all(15),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(15),
              color: Colors.white,
              border: Border.all(color: mainColor, width: 2)
            ),
            child: Icon(Icons.turned_in_not, color: mainColor, size: 25)
          )
        ]
      )
    );
  }
}

//----MODELS----

class MountModel {
  String path;
  String name;
  String location;
  String description;

  MountModel({
    this.path = '',
    this.name = '',
    this.location = '',
    this.description = ''
  });
}

class CategoryModel {
  String category;
  IconData? icon;

  CategoryModel({this.category = '', this.icon});
}

class AppBottomBarItem {
  IconData? icon;
  bool isSelected; 
  String label;

  AppBottomBarItem({
    this.icon,
    this.label = '',
    this.isSelected = false
  });
}