Intermediate Style in Flutter

Summary

In this lesson we'll cover:

The Code for This Lesson

You can check out the step/step08 branch here which will contain the code for this lesson.

Adding More Fixture Data

  1. We'll need to add information to each location so that we have more to display when we decide to style things more, so update the existing list of locations with the following:
// lib/models/location.dart

import './location_fact.dart';
import 'package:meta/meta.dart';

/// Represents a tourism location a user can visit.
class Location {
  final int id;
  final String name;
  final String imagePath;
  final String userItinerarySummary;
  final String tourPackageName;
  final List<LocationFact> facts;

  Location({
    this.id,
    this.name,
    this.imagePath,
    this.userItinerarySummary,
    this.tourPackageName,
    this.facts,
  });

  static List<Location> fetchAll() {
    return [
      Location(
          id: 1,
          name: 'Kiyomizu-dera',
          imagePath: 'assets/images/kiyomizu-dera.jpg',
          userItinerarySummary: 'Day 1: 4PM - 5:00PM',
          tourPackageName: 'Standard Package',
          facts: [
            LocationFact('Summary',
                'Kiyomizu-dera, officially Otowa-san Kiyomizu-dera, is an independent Buddhist temple in eastern Kyoto. The temple is part of the Historic Monuments of Ancient Kyoto UNESCO World Heritage site.'),
            LocationFact(
                'Architectural Style', 'Japanese Buddhist architecture.'),
          ]),
      Location(
          id: 2,
          name: 'Mount Fuji',
          imagePath: 'assets/images/fuji.jpg',
          userItinerarySummary: 'Day 1: 9AM - 1:30PM',
          tourPackageName: 'Standard Package',
          facts: [
            LocationFact('Summary',
                'Japan’s Mt. Fuji is an active volcano about 100 kilometers southwest of Tokyo. Commonly called “Fuji-san,” it’s the country’s tallest peak, at 3,776 meters. A pilgrimage site for centuries, it’s considered one of Japan’s 3 sacred mountains, and summit hikes remain a popular activity. Its iconic profile is the subject of numerous works of art, notably Edo Period prints by Hokusai and Hiroshige.'),
            LocationFact('Did You Know',
                'There are three cities that surround Mount Fuji: Gotemba, Fujiyoshida and Fujinomiya.'),
          ]),
      Location(
          id: 3,
          name: 'Arashiyama Bamboo Grove',
          imagePath: 'assets/images/arashiyama.jpg',
          userItinerarySummary: 'Day 1: 2PM - 3:30PM',
          tourPackageName: 'Standard Package',
          facts: [
            LocationFact('Summary',
                'While we could go on about the ethereal glow and seemingly endless heights of this bamboo grove on the outskirts of Kyoto, the sight\'s pleasures extend beyond the visual realm.'),
            LocationFact('How to Get There',
                'Kyoto airport, with several terminals, is located 16 kilometres south of the city and is also known as Kyoto. Kyoto can also be reached by transport links from other regional airports.'),
          ]),
    ];
  }

  static Location fetchByID(int locationID) {
    // NOTE: this will replaced by a proper API call
    List<Location> locations = Location.fetchAll();
    for (var i = 0; i < locations.length; i++) {
      if (locations[i].id == locationID) {
        return locations[i];
      }
    }
    return null;
  }
}
  1. Here, we have added more fields to our model and have an updated constructor as well, with all optional parameters.

Using ListView.builder()

  1. In order to efficiently render many list view items, it's best to use ListView.builder() named constructor. Let's do that like so:
// lib/screens/locations/locations.dart

// ...

return Scaffold(
  // ...
  body: ListView.builder(
    itemCount: locations.length,
    itemBuilder: (context, index) =>
        _itemBuilder(context, locations[index]),
  ),
);

// ...

Implementing an itemBuilder

  1. Here's the code for rendering each list view item. Simply add this method:

// ...
// lib/screens/locations/locations.dart

//  ...

Widget _itemBuilder(BuildContext context, Location location) {
  return GestureDetector(
    onTap: () => _onLocationTap(context, location.id),
    child: Container(
      height: 245.0,
      child: Stack(
        children: [
          ImageBanner(assetPath: location.imagePath, height: 245.0),
          TileOverlay(location),
        ],
      ),
    ),
  );
}

// ...

Re-using Our LocationTile Widget in LocationDetail

  1. We can now adding a SingleChildScrollView with a LocationTile, which is already used by another widget, TileOverlay.
  2. Update the LocationDetail screen's Scaffold implementation with:
// lib/screens/location_detail/location_detail.dart

// ...

return Scaffold(
        // ...
        body: SingleChildScrollView(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                ImageBanner(assetPath: location.imagePath),
                Padding(
                  padding:
                      EdgeInsets.symmetric(vertical: 20.0, horizontal: 4.0),
                  child: LocationTile(location: location),
                ),
              ]..addAll(textSections(location))),
        ));

// ...

  1. Here, we add a Padding widget, allowing us to provide some breathing room to our top section of the screen.
  2. The SingleChildScrollView allows us to easily scroll if the context goes beyond the bounds of the screen.

Finally, Adding More Style!

  1. Here, we simply expand upon our use of style in our existing lib/style.dart file.
  2. Simply overwrite the existing file with the following:
// lib/style.dart

import 'package:flutter/material.dart';

const LargeTextSize = 22.0;
const MediumTextSize = 16.0;
const SmallTextSize = 12.0;

const String FontNameDefault = 'Montserrat';

const Color TextColorDark = Colors.black;
const Color TextColorLight = Colors.white;
const Color TextColorAccent = Colors.red;
const Color TextColorFaint = Color.fromRGBO(125, 125, 125, 1.0);

const DefaultPaddingHorizontal = 12.0;

const AppBarTextStyle = TextStyle(
  fontFamily: FontNameDefault,
  fontWeight: FontWeight.w300,
  fontSize: MediumTextSize,
  color: Colors.white,
);

const TitleTextStyle = TextStyle(
  fontFamily: FontNameDefault,
  fontWeight: FontWeight.w300,
  fontSize: LargeTextSize,
  color: TextColorDark,
);

const SubTitleTextStyle = TextStyle(
  fontFamily: FontNameDefault,
  fontWeight: FontWeight.w300,
  fontSize: MediumTextSize,
  color: TextColorAccent,
);

const CaptionTextStyle = TextStyle(
  fontFamily: FontNameDefault,
  fontWeight: FontWeight.w300,
  fontSize: SmallTextSize,
  color: TextColorDark,
);

const Body1TextStyle = TextStyle(
  fontFamily: FontNameDefault,
  fontWeight: FontWeight.w300,
  fontSize: MediumTextSize,
  color: Colors.black,
);
  1. Now, add the new style to lib/app.dart by adding:
// lib/app.dart

// ...

ThemeData _theme() {
  return ThemeData(
      // ...
        subtitle: SubTitleTextStyle,
        caption: CaptionTextStyle,
      // ...
}

Conclusion

We did a lot of coding in this lesson. Next, we'll learn some new concepts, namely how to test our app properly.