LandingPage

What You'll Build in this Codelab:

For the Coding Roulette Session:

Let's show a schematic view of what we'll be tackling with the LandingPage widget below:

LandingPage

We'll build a simple landing page for our application. Initially, this app displays a splash screen, waits two seconds, then proceeds to show the landing page we'll create below. Afterwards, this landing page will navigate users to a list page.

Let's start!

Assuming you've already completed the previous labs, you should already have the project's main MaterialApp launch the SplashScreen page initially. For development purposes, let's point the MaterialApp's home property to the new page we'll create, which we'll call LandingPage. Go ahead and in the main method, inside the MaterialApp, replace the home property from SplashPage to LandingPage (or comment it out since we'll need it at the end) as follows:

LandingPage

Negative Don't worry about the red squiggly line under the LandingPage widget; we haven't created this class yet, that's why it's complaining. We'll create it in a minute.

Let's proceed now and create the corresponding class.

Let's create the class that represents this new widget. Anywhere in this file (most conveniently at the bottom), add a new class called LandingPage that extends StatelessWidget. As usual, override its build method, and return some placeholder content: a Scaffold widget and for its body property, a Text widget with the content "Landing Page":

LandingPage

Positive In a full-blown Flutter project, of course you'd create separate files for each of your widgets, which is the recommended approach. Since we're doing this in DartPad, we create all classes on a single file.

Running whatever we have in DartPad, then viewing the output on the Preview Page, you should get the following:

LandingPage

Click Next and let's move on!

We'll eliminate our placeholder Text widget from the Scaffold's body property and replace it by the root of the body structure, which will be a Stack widget. We'll use a Stack widget since we'll lay out the children widgets one of top of each other in a layered fashion, starting with the background image.

First replace the Text widget with a Stack widget with empty *children property (assign it an empty array):

LandingPage

As the first child of this Stack widget, and going by our design, we will have a background image as the bottommost element, then all the elements will go at the top.

Add a Container widget that will serve as the host of the image, which will be set as its background, by using the decoration property and set the BoxDecoration's image property to a DecorationImage, whic in turn we'll set a NetworkImage widget to the image's property of the DecorationImage widget. Set the DecorationImage's fit property to BoxFit.cover so it covers the whole Container area but without losing its aspect ratio:

LandingPage

Positive For the image, you can use the image in this link or from a similar royalty-free image site.

LandingPage

Running whatever we have accomplished so far in DartPad, you get the following in the Preview:

LandingPage

LandingPage

We want to add a simple Container widget with a faded color to give a color overlay effect. This is easily accomplished by adding another Container widget to the Stack (on top of the image) and just lowe its opacity.

Simple as adding a Container widget, and set its color property to the mainThemeColor and set a 70% opacity by using the withOpacity method of the Color object:

LandingPage

Check out how it came out by running it on DartPad now:

LandingPage

LandingPage

So far so good, so it's time to lay down the content pieces. On top of the background image and color overlay, we'll place a Column widget with a bunch of content. Add the Column widget and set both main axis and cross axis alignments to center and stretch respectively. Add an empty children property for now:

LandingPage

Since this is a Column widget, its items will be laid out vertically, from top to bottom.

Start by adding the first child to this Column: a Text widget; center aligned, and with a TextStyle with a white color, bold font, and font size 30px; add the content "Paradise" to it:

LandingPage

Let's keep adding items to this Column. We want to leave some breathing space between the items we'll be placing, so add a SizedBox with 60px height to denote some spacing here - add it right under the previous Text widget:

LandingPage

Moving right along, we'll add the app's designated icon (Icons.pool) with a color white with 80px in size; and while you're at it, add a SizedBox with 10px height for spacing purposes:

LandingPage

Let's add another Text widget for another label, with centered text, white color with a 50% opacity, and all the letters in uppercase. Since you're in that area, add another spacing SizedBox widget with 5px in height, as follows:

LandingPage

Let's add another Text widget, also with its text centered, white, 30px and bold, with the content "Find a Hotel". Add also another SizedBox with 20px height right underneath it:

LandingPage

If you run the app in DartPad at this moment, you should see the following on the Preview Pane:

LandingPage

Looking good! Now as the last item in this Column widget, and following our design, we'll add the search bar. But instead of building it inside of the Column using the core widgets available, we'll actually encapsulated into its own custom widget, which we'll call LandingSearchBar.

We'll work on this custom widget on the next step. Click Next to proceed.

The schematic figure below shows how you'd build a custom search bar layout (just the layout, no editable text field) and encapsulate its structure within a custom widget.

LandingPage

Let's pick it up where we left off on the previous step.

As the last item of the Column widget, add a placeholder widget called LandingSearchBar. We will create it in a minute:

LandingPage

Let's create now the corresponding class for this landing search bar widget. Create a class called LandingSearchBar that extends StatelessWidget as customary for custom widgets that won't hold state within themselves. As usual, return a dummy Container widget as the return statement from the overridden *build method:

LandingPage

Now let's proceed to further define the structure of this widget.

Add left and right margin, 30px to this Container:

LandingPage

Add padding of 5px (top, bottom and right), and 20px (left):

LandingPage

For the white background on the main Container and the rounded corners, use the decoration property and set it as a BoxDecoration; set its color to white, and borderRadius of 50:

LandingPage

Now let's focus on the contents of this Container.

As the child of this Container, add a Row widget - this is what will hold the placeholder text (a Text widget) and a circular search button (a styled Container), using a spaceBetween strategy laying down its children on the main axis; set the Row's children property to an empty array for now:

LandingPage

First child of this row will be the Search Hotel Text widget, so add the Text widget, content should say "Search Hotel", with a grey color:

LandingPage

Let's take a pause to run what we have on DartPad, and confirm that you can see the following:

LandingPage

Creating the round search bar button for the LandingSearchBar widget

LandingPage

We'll create a custom round tappable button with an icon that our users will be able to tap, which will perform a navigation to a list page once we're done.

Let's build it!

Inside the Row widget above, right next to the Text widget we created, add a Container widget, dimensions 30px by 30px, and use the same decoration property to apply both the background color (mainThemeColor) of the Container and border radius (BorderRadius.circular(25)); this applies the roundness to the Container making it look like a circle.

LandingPage

Add an Icon widget as the child of this styled Container; add the icon Icons.search, with a white color and 15px in size:

LandingPage

Adding tapping capability to your custom widgets

In order to allow your widget elements to capture user gestures and events (i.e. become tappable and trigger events), you can wrap them inside a handy widget called GestureDetector. Since we want to make our rounded Container with the search icon tappable, wrap it inside a GestureDetector widget by setting this Container as the child of the GestureDetector widget; hook up its onTap event to an empty callback for now, as follows:

LandingPage

Hook up the onTap event on the GestureDetector

Now it's time to implement the onTap event: we'll perform a simple navigation to the next page in turn (ListPage) upon tapping on our custom circular search button.

Inside the onTap event, use the Navigator.context provision to access the navigator context provided by Flutter, and push a MaterialPageRoute, which wraps the page that we want to navigate to - the ListPage widget, as follows:

LandingPage

Now, if you take this for a spin, you should be able to tap on the circular search button inside the LandingSearchBar widget, which in turn will navigate you to the ListPage we created on lab #2. The screenshot below is what you should be seeing after completing the above steps:

LandingPage

LandingPage

In order to stay true to the design, we will have to add a simple AppBar and Drawer widget to the LandingPage's Scaffold widget - two of the essential widgets that get added to a Scaffold to consider a Material-based UI design complete.

Let's proceed.

Adding the AppBar widget

Usually the AppBar gets assigned to the Scaffold widget by setting its appBar property to an AppBar widget. An AppBar widget - just like the name implies - is the application's topmost bar, where we can place a title, navigation icons, menu icons, etc.

In our case, we want the AppBar to look on top of the background image; for this effect to occur, we must add it to the existing Stack widget, right under the Column widget.

Let's proceed by adding the AppBar to the Stack and setting the following properties to it:

Your code should look as follows:

LandingPage

Taking it for a spin on DartPad by running it, it should look like this:

LandingPage

Notice how the AppBar shows transparent, but shows the back chevron icon to go back to the splash screen. In order to make it show the hamburger menu to expand the drawer menu widget, you must add the Drawer widget.

Let's do that now.

Adding the Drawer Widget

Inside of the Scaffold, let's populate now the drawer property by assigning to it a Drawer widget. The Drawer widget is a panel that slides out horizontally from the edge of a Scaffold widget, used for menus, navigation links, etc.

Your code should look as follows:

LandingPage

Now try expanding the drawer by tapping on the hamburger menu. It looks a little plain now.

LandingPage

Let's add some content to this widget.

Adding some content to the Drawer

Let's add some content to the Drawer widget and customize it a little bit. A Drawer is pretty much like any other single-child widget, in which you can add anything inside it and it will render as you please.

First, let's start by adding a Container widget as the child of the Drawer widget, and set its padding to 20px all around using EdgeInsets.all(20). By default, its background color is white so let's leave it as is. Align its contents to the bottom left corner by setting its alignment property to Alignment.bottomLeft:

LandingPage

Now, add an Icon widget as the child of the newly added Container widget, with our specific icon (Icons.pool), color of mainThemeColor and size 80. Your code should look like this:

LandingPage

And with that, you should be all set with customizing the Drawer widget. You can feel free to add further elements and links to your own Drawer widgets, since it provides a lot of flexibility for customization. Check it out after running it on DartPad:

LandingPage

Sweet! let's wrap things up on the next step. Click Next to continue.

LandingPage

Let's set up the app's final workflow which is:

In order to accomplish this, let's start by reverting the change we made in the main method, inside the MaterialApp's home property - set this property back to be the SplashPage instead of the LandingPage:

LandingPage

In the SplashPage, inside the Future.delayed() method, instead of navigating to the ListPage after the 2 seconds have ellapsed, change it to point to the LandingPage widget:

LandingPage

And with that, we wrap up the codelab series for this workshop, where we accomplished the following:

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

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

import 'package:flutter/material.dart';

const Color mainThemeColor = Color(0xFF272D8D);

final List<Attraction> attractionsList = [
  Attraction(
      imgPath:
          'https://cf.bstatic.com/xdata/images/hotel/max1024x768/275162028.jpg?k=38b638c8ec9ec86624f9a598482e95fa634d49aa3f99da1838cf5adde1a14521&o=&hp=1',
      name: 'Grand Bavaro Princess',
      desc: 'All-Inclusive Resort',
      location: 'Punta Cana, DR',
      rating: 3,
      price: 80.0),
  Attraction(
      imgPath:
          'https://cf.bstatic.com/xdata/images/hotel/max1024x768/232161008.jpg?k=27808fe44ab95f6468e5433639bf117032c8271cebf5988bdcaa0a202b9a6d79&o=&hp=1',
      name: 'Hyatt Ziva Cap Cana',
      desc: 'All-Inclusive Resort',
      price: 90.0,
      rating: 4,
      location: 'Punta Cana, DR'),
  Attraction(
      imgPath:
          'https://cf.bstatic.com/xdata/images/hotel/max1024x768/256931299.jpg?k=57b5fb9732cd89f308def5386e221c46e52f48579345325714a310addf819274&o=&hp=1',
      name: 'Impressive Punta Cana',
      desc: 'All-Inclusive Resort',
      price: 100.0,
      rating: 5,
      location: 'Punta Cana, DR'),
   Attraction(
      imgPath:
          'https://cf.bstatic.com/xdata/images/hotel/max1024x768/283750757.jpg?k=4f3437bf1e1b077463c9900e4dd015633db1d96da38f034f4b70a4ba3ef76d82&o=&hp=1',
      name: 'Villas Mar Azul Dreams',
      desc: 'All-Inclusive Resort',
      price: 100.0,
      rating: 4,
      location: 'Tallaboa, PR'),
];

final List<BottomBarItem> barItemsList = [
  BottomBarItem(label: 'Home', isSelected: true, icon: Icons.home),
  BottomBarItem(label: 'Account', isSelected: false, icon: Icons.person),
  BottomBarItem(label: 'Bookings', isSelected: false, icon: Icons.pending_actions),
  BottomBarItem(label: 'Payments', isSelected: false, icon: Icons.payments),
  BottomBarItem(label: 'More', isSelected: false, icon: Icons.more_horiz),
];

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false, 
      home: SplashPage()
    )
  );
}

class SplashPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

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

    return Stack(
      children: [
        Container(
          color: mainThemeColor
        ),
        Align(
          alignment: Alignment.center,
          child: Icon(
            Icons.pool, 
            color: Colors.white, 
            size: 80
          )
        ),
        Align(
          alignment: Alignment.bottomCenter,
          child: LinearProgressIndicator(
            valueColor: AlwaysStoppedAnimation<Color>(
              Colors.white.withOpacity(0.4)
            )
          )
        )
      ],
    );
  }
}

class LandingPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: Drawer(
        child: Container(
          padding: EdgeInsets.all(20),
          alignment: Alignment.bottomLeft,
          child: Icon(Icons.pool, color: mainThemeColor, size: 80)
        )
      ),
      body: Stack(
        children: [
          Container(
            decoration: BoxDecoration(
              image: DecorationImage(
                image: NetworkImage('https://images.pexels.com/photos/261394/pexels-photo-261394.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'),
                fit: BoxFit.cover
              )
            )
          ),
          Container(color: mainThemeColor.withOpacity(0.7)),
          AppBar(
            backgroundColor: Colors.transparent,
            elevation: 0,
            iconTheme: IconThemeData(color: Colors.white),
          ),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text('Paradise',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 30
                )
              ),
              SizedBox(height: 60),
              Icon(Icons.pool, color: Colors.white, size: 80),
              SizedBox(height: 10),
              Text('Choose location to'.toUpperCase(),
                textAlign: TextAlign.center,
                style: TextStyle(color: Colors.white.withOpacity(0.5))
              ),
              SizedBox(height: 5),
              Text('Find a Hotel', textAlign: TextAlign.center,
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 30, 
                  fontWeight: FontWeight.bold
                )
              ),
              SizedBox(height: 20),
              LandingSearchBar()
            ]
          ),
        ],
      )
    );  
  }
}

class LandingSearchBar extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(left: 30, right: 30),
      padding: EdgeInsets.only(top: 5, bottom: 5, left: 20, right: 5),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(50)
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('Search hotel', style: TextStyle(color: Colors.grey)),
          GestureDetector(
            onTap: () {
              Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) => ListPage()));
            },
            child: Container(
              width: 30, height: 30,
              child: Icon(Icons.search,color: Colors.white, size: 15),
              decoration: BoxDecoration(
                color: mainThemeColor,
                borderRadius: BorderRadius.circular(25)
              )
            )
          )
          
        ]
      )
    );
  }
}

class ListPage extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        iconTheme: IconThemeData(color: Colors.white),
        elevation: 0,
        title: Icon(Icons.pool, color: Colors.white),
        actions: [
          Container(
            margin: EdgeInsets.only(right: 15),
            child: Icon(
              Icons.notifications, 
              color: Colors.white
            )
          )
        ]
      ),
      backgroundColor: mainThemeColor,
      body: ClipRRect(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(50),
          topRight: Radius.circular(50)
        ),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(50),
              topRight: Radius.circular(50)
            )
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: attractionsList.length,
                  itemBuilder: (context, index) {
                    Attraction attr = attractionsList[index];
                    return AttractionCard(attraction: attr);
                  })
              ),
              BottomBarWidget()
            ]
          )
        )
      )
    );
  }
}

class AttractionCard extends StatelessWidget {

  Attraction? attraction;
  AttractionCard({ this.attraction });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(40),
        boxShadow: [
          BoxShadow(
            blurRadius: 20,
            offset: Offset.zero,
            color: Colors.black.withOpacity(0.1)
          )
        ]
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(40),
        child: Container(
          height: 300,
          child: Stack(
            children: [
              Column(
                children: [
                  Container(
                    height: 150,
                    decoration: BoxDecoration(
                      image: DecorationImage(
                        image: NetworkImage(attraction!.imgPath!),
                        fit: BoxFit.cover
                      )
                    )
                  ),
                  Container(
                    height: 150,
                    padding: EdgeInsets.all(20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(attraction!.name!, 
                              style: TextStyle(
                                color: Colors.black,
                                fontSize:14,
                                fontWeight:FontWeight.bold
                              )
                            ),
                            SizedBox(width: 5),
                            Row(
                              children: [
                                Icon(
                                  Icons.pin_drop,
                                  color: Colors.grey.withOpacity(0.7),
                                  size: 12
                                ),
                                SizedBox(width: 5),
                                Text(attraction!.location!,
                                  style: TextStyle(
                                    fontSize: 12, 
                                    color: Colors.grey.withOpacity(0.7), 
                                    fontWeight: FontWeight.bold
                                  )
                                ),
                              ]
                            ),
                            SizedBox(width: 5),
                            RatingWidget(rating: attraction!.rating!)
                          ]
                        ),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.end,
                          mainAxisAlignment: MainAxisAlignment.end,
                          children: [
                            Text('\$${attraction!.price!.toStringAsFixed(2)}',
                              style: TextStyle(
                                color: Colors.black,
                                fontSize:16,
                                fontWeight: FontWeight.bold
                              )
                            ),
                            SizedBox(height: 5),
                            Text('Per Night',
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey.withOpacity(0.7),
                                fontWeight: FontWeight.bold
                              )
                            )
                          ],
                        )
                      ]
                    )
                  )
                ],
              ),
              Align(
                alignment: Alignment.centerRight,
                child: Container(
                  child: Icon(
                    Icons.favorite,
                    color: Colors.white,
                    size: 15
                  ),
                  margin: EdgeInsets.only(right: 10),
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                    color: mainThemeColor,
                    borderRadius: BorderRadius.circular(40),
                    boxShadow: [
                      BoxShadow(
                        blurRadius: 10,
                        color: mainThemeColor.withOpacity(0.5),
                        offset:Offset.zero
                      )
                    ]
                  )
                )
              )
            ],
          )
        )
      )
    );
  }
}

class BottomBarWidget extends StatefulWidget {
  @override
  BottomBarWidgetState createState() => BottomBarWidgetState();
}

class BottomBarWidgetState extends State<BottomBarWidget> {

  List<BottomBarItem> barItems = barItemsList;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 15),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,

        //... rest of the code omitted for brevity ...
        children: List.generate(
          barItems.length,
          (index) {
              
            var barItem = barItems[index];

            return GestureDetector(
              onTap: () {
                setState(() {
                  barItems.forEach((element) {
                      element.isSelected = barItem == element;
                  });
                });
              },
              child: Column(
                children: [
                  Icon(barItem.icon, color: barItem.isSelected! 
                    ? mainThemeColor : Colors.grey),
                  Text(barItem.label!, style: TextStyle(
                    color: (barItem.isSelected! ? mainThemeColor : Colors.grey), 
                    fontSize: 11
                  ))
                ]
              )
            );
        })
      )
    );
  }
}

class RatingWidget extends StatelessWidget {

  int? rating;
  RatingWidget({ this.rating });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Row(
          children: List.generate(5, (index) {
            return Icon(
              index < this.rating! ? Icons.star : Icons.star_border,
              color: Colors.yellow
            );
          })
        ),
        SizedBox(width: 5),
        Text('${this.rating!}/5 Reviews',
           style: TextStyle(
             fontSize: 12, 
             color: Colors.grey.withOpacity(0.7)
          )
        )
      ],
    );
  }
}

class Attraction {
  String? imgPath;
  String? name;
  String? desc;
  double? price;
  String? location;
  int? rating;

  Attraction({this.imgPath, this.name, this.desc, this.price, this.location, this.rating });
}

class BottomBarItem {
  String? label;
  bool? isSelected;
  IconData? icon;
  
  BottomBarItem({ this.label, this.isSelected, this.icon });
}