In the previous article, we learned how to create a card to show some information about a planet. Now it is time to put that information in the card.

Job description

We want to be able to provide the planet info to PlanetRow widget and see it painted on screen.

Creating a data source

As the list of planets is static and the data won’t change, creating this information from code will be enough.

First, we create a class to hold information for a single planet:

 1class Planet {
 2  final String id;
 3  final String name;
 4  final String location;
 5  final String distance;
 6  final String gravity;
 7  final String description;
 8  final String image;
 9
10  const Planet({this.id, this.name, this.location, this.distance, this.gravity,
11    this.description, this.image});
12}

Fine, just a simple object (PODO? Plain Old Dart Object?).

Now, we create the list of planets:

 1List<Planet> planets = [
 2  const Planet(
 3    id: "1",
 4    name: "Mars",
 5    location: "Milkyway Galaxy",
 6    distance: "227.9m Km",
 7    gravity: "3.711 m/s ",
 8    description: "Lorem ipsum...",
 9    image: "assets/img/mars.png",
10  ),
11  const Planet(
12    id: "2",
13    name: "Neptune",
14    location: "Milkyway Galaxy",
15    distance: "54.6m Km",
16    gravity: "11.15 m/s ",
17    description: "Lorem ipsum...",
18    image: "assets/img/neptune.png",
19  ),
20  const Planet(
21    id: "3",
22    name: "Moon",
23    location: "Milkyway Galaxy",
24    distance: "54.6m Km",
25    gravity: "1.622 m/s ",
26    description: "Lorem ipsum...",
27    image: "assets/img/moon.png",
28  ),
29  const Planet(
30    id: "4",
31    name: "Earth",
32    location: "Milkyway Galaxy",
33    distance: "54.6m Km",
34    gravity: "9.807 m/s ",
35    description: "Lorem ipsum...",
36    image: "assets/img/earth.png",
37  ),
38  const Planet(
39    id: "5",
40    name: "Mercury",
41    location: "Milkyway Galaxy",
42    distance: "54.6m Km",
43    gravity: "3.7 m/s ",
44    description: "Lorem ipsum...",
45    image: "assets/img/mercury.png",
46  ),
47];

As you see, all the info is just mocked data. I invite you to use the real information if you want to.

Also, we place the images on the img folder. Find all of them in the repository.

Ok, now we are ready to modify the PlanetRow class.

A PlanetRow for all planets

We need to make the PlanetCard to be able to receive one Planet object and paint it:

 1class PlanetRow extends StatelessWidget {
 2
 3  final Planet planet;
 4
 5  PlanetRow(this.planet);
 6  
 7  ...
 8
 9  @override
10  Widget build(BuildContext context) {
11    return new Container(
12      height: 120.0,
13      margin: const EdgeInsets.symmetric(
14        vertical: 16.0,
15        horizontal: 24.0,
16      ),
17      child: new Stack(
18        children: <Widget>[
19          planetCard
20          planetThumbnail,
21        ],
22      )
23    );
24  }
25}

First, we add a parameter to the constructor that receives a Planet object and stores it in a final field.

Next step, we will modify the planetThumbnail:

 1  final planetThumbnail = new Container(
 2    margin: new EdgeInsets.symmetric(
 3      vertical: 16.0
 4    ),
 5    alignment: FractionalOffset.centerLeft,
 6    child: new Image(
 7      image: new AssetImage(planet.image),
 8      height: 92.0,
 9      width: 92.0,
10    ),
11  );

The only change we have applied is to modify the thumbnail path from a constant to the field image in the Planet object we received.

Also, in the previous article, we declared planetThumbnail and planetCard as final class members, in order to be able to reference the planet field, this declaration should be moved into the build function.

1class HomePageBody extends StatelessWidget {
2  @override
3  Widget build(BuildContext context) {
4    return new PlanetRow(planets[0]);
5  }
6}

And finally, we modify the creation of PlanetRow in HomePageBody to give a planet of the array to it. You can try to change the 0 to another number to see how the thumbnail works.

Let’s add the rest of fields to the card.

A cute card

As we will be using three different text styles, we will create them as constants and reuse them later.

Text styling

We add the Poppins-regular.ttf file to assets/fonts and add an entry to the pubspec.yml, so the fonts entry finally looks like this:

  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Poppins-Regular.ttf
          weight: 400

If you don’t know the weight of a font, a trick is to check the class FontWeight, as each constant (w100, w200, etc..) indicates the usual preffix to the font name associated to each weight (thin, extra-light, light, bold, regular, etc).

As all our text styles use the same font, we can create a base style indicating the font:

    final baseTextStyle = const TextStyle(
      fontFamily: 'Poppins'
    );

Now we can create the header style copying the base style:

    final headerTextStyle = baseTextStyle.copyWith(
      color: const Colors.white,
      fontSize: 18.0,
      fontWeight: FontWeight.w600
    );

The copyWith allows us to generate a new TextStyle object from another one modifying some of the attributes.

Same for the regular text style:

    final regularTextStyle = baseTextStyle.copyWith(
      color: const Color(0xffb6b2df),
      fontSize: 9.0,
      fontWeight: FontWeight.w400
    );

And, for the subheader, we can copy the regular one just changing the size of the font:

    final subHeaderTextStyle = regularTextStyle.copyWith(
      fontSize: 12.0
    );

This is a far from ideal way to organize text styles, but it’s better than nothing. In a future article we will discuss how to organize and centralize all the styling to save a lot of boilerplate.

And now the content

I’ve modified some of the margins on the design to get better dp sizes (basically, approach them to multiples of 3).

card content measures

The result is the following code:

final planetCardContent = new Container(
      margin: new EdgeInsets.fromLTRB(76.0, 16.0, 16.0, 16.0),
      constraints: new BoxConstraints.expand(),
      child: new Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          new Container(height: 4.0),
          new Text(planet.name,
            style: headerTextStyle,
          ),
          new Container(height: 10.0),
          new Text(planet.location,
            style: subHeaderTextStyle

          ),
          new Container(
            margin: new EdgeInsets.symmetric(vertical: 8.0),
            height: 2.0,
            width: 18.0,
            color: new Color(0xff00c6ff)
          ),
          new Row(
            children: <Widget>[
              new Image.asset("assets/img/ic_distance.png", height: 12.0),
              new Container(width: 8.0),
              new Text(planet.distance,
                style: regularTextStyle,
              ),
              new Container(width: 24.0),
              new Image.asset("assets/img/ic_gravity.png", height: 12.0),
              new Container(width: 8.0),
              new Text(planet.gravity,
                style: regularTextStyle,
              ),
            ],
          ),
        ],
      ),
    );

What is hapenning here?

  • We create a container that will be the base widget for the whole content.
  • We define the margins as per design.
  • We have to define a constraint (BoxConstraints.expand()), otherwise, the container will adjust to the minimum size required by its children, and we want it to get the whole row.
  • As each text is below the other, we use a Column to dispose them.
  • We use empty containers to create the separation between text elements.
  • First element shows the name of the planet using headerTextStyle.
  • Second element shows the location of the planet using subHeaderTextStyle.
  • We use a single container to create the blue line, just specifying the margins and size of it, and giving the appropiate background color.
  • The gravity and distance should be put in a Row, as they flow horizontally.
  • Each consists on an icon from assets, a separation container, and a text.

There is a much more convenient way of doing the row of values (gravity and distance).

Imagine we want the second value to always start in the center, despite the width of the row. In order to achieve this, we have to use a Expanded widget:

          new Row(
            children: <Widget>[
              new Expanded(
                child: new Row(
                  children: <Widget>[
                    new Image.asset("assets/img/ic_distance.png", height: 12.0),
                    new Container(width: 8.0),
                    new Text(planet.distance, style: regularTextStyle),
                  ]
                ),
              ),
              new Expanded(
                child: new Row(
                  children: <Widget>[
                    new Image.asset("assets/img/ic_gravity.png", height: 12.0),
                    new Container(width: 8.0),
                    new Text(planet.gravity, style: regularTextStyle),
                  ]
                ),
              )
            ],
          ),

Now, the row contains two Expanded widgets. And they will share the space to fill at 50%. As the content aligns to the left, the second one will always start at the center.

As both contents are very similar, we can extract them to a function:

    Widget _planetValue({String value, String image}) {
      return new Row(
        children: <Widget>[
          new Image.asset(image, height: 12.0),
          new Container(width: 8.0),
          new Text(planet.gravity, style: regularTextStyle),
        ]
      );
    }

So, the final row looks like this:

          new Row(
            children: <Widget>[
              new Expanded(
                child: _planetValue(
                  value: planet.distance,
                  image: 'assets/img/ic_distance.png')

              ),
              new Expanded(
                child: _planetValue(
                  value: planet.gravity,
                  image: 'assets/img/ic_gravity.png')
              )
            ],
          )

It looks cleaner to me.

The final result now looks like this:

final card

To be continued

In this episode we have learned several things related to layout and styling:

  • How to use different weights of the same font. If you come from Android native development, this looks like a small miracle ;).
  • How to simplify text styling by overriding styles. We will see a lot more on how to make this even easier and cleaner.
  • How to create and use a small data source so that we can paint any planet.
  • Using constrains in a container to make it expand.
  • Using the expanded widget to distribute items equally.

The whole code for this project is uploaded to the flutter-planets-tutorial repository, and this article is in the branch Lesson_3_Planets-Flutter_adding_content_to_the_card. Also, you will find all the assets used in the project.

In the next article, we will finally create a list of elements to show all the planets related information.

Stay tuned!