Home Flutter Books Flutter Apprentice

2
Hello, Flutter Written by Michael Katz

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Now that you’ve had a short introduction, you’re ready to start your Flutter apprenticeship. Your first task is to build a basic app from scratch, giving you the chance to get the hang of the tools and the basic Flutter app structure. You’ll customize the app and find out how to use a few popular widgets like ListView and Slider to update its UI in response to changes.

Creating a simple app will let you see just how quick and easy it is to build cross-platform apps with Flutter — and it will give you a quick win.

By the end of the chapter, you’ll have built a lightweight recipe app. Since you’re just starting to learn Flutter, your app will offer a hard-coded list of recipes and let you use a Slider to recalculate quantities based on the number of servings.

All you need to start this chapter is to have Flutter set up. If the flutter doctor results show no errors, you’re ready to get started. Otherwise, go back to Chapter 1, “Introduction”, to set up your environment.

Creating a new app

There are two simple ways to start a new Flutter app. In the last chapter, you created a new app project through the IDE. Alternatively, you can create an app with the flutter command. You’ll use the second option here.

Open a terminal window, then navigate to the location where you want to create a new folder for the project. For example, you can use this book’s materials and go to flta-materials/02-hello-flutter/projects/starter/`.

Creating a new project is straightforward. In the terminal, run:

flutter create Recipes

This command creates a new app in a new folder, both named Recipes. It has the demo app code, as you saw in the previous chapter, with support for running on iOS and Android.

Using your IDE, open the Recipes folder as an existing project.

Build and run and you’ll see the same demo app as in Chapter 1, “Introduction”.

Tapping the Plus button increments the counter.

Making the app yours

The ready-made app is a good place to start because the flutter create command puts all the boilerplate together for you to get up and running. But this is not your app. It’s literally MyApp, as you can see near the top of main.dart:

class MyApp extends StatelessWidget {
void main() {
  runApp(RecipeApp());
}
class RecipeApp extends StatelessWidget {

Styling your app

To continue making this into a new app, you’ll customize the the appearance of your widgets next. Replace RecipeApp’s build() with:

// 1
@override
Widget build(BuildContext context) {
  // 2
  return MaterialApp(
    // 3
    title: 'Recipe Calculator',
    theme: ThemeData(
      // 4
      primaryColor: Colors.white,
      accentColor: Colors.black
    ),
    // 5
    home: MyHomePage(title: 'Recipe Calculator'),
  );
}

Clearing the app

You’ve themed the app, but it’s still displaying the counter demo. Clearing the screen is your next step. To start, replace the _MyHomePageState class with:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    // 1
    return Scaffold(
      // 2
      appBar: AppBar(
        title: Text(widget.title),
      ),
      // 3
      body: SafeArea(
        // 4
        child: Container(),
        ),
    );
  }
}

Building a recipe list

An empty recipe app isn’t very useful. The app should have a nice list of recipes for the user to scroll through. Before you can display these, however, you need the data to fill out the UI.

Adding a data model

You’ll use Recipe as the main data structure for recipes in this app.

class Recipe {
  String label;
  String imageUrl;

  Recipe(this.label, this.imageUrl);
}
static List<Recipe> samples = [
  Recipe("Spaghetti and Meatballs", "assets/2126711929_ef763de2b3_w.jpg"),
  Recipe("Tomato Soup", "assets/27729023535_a57606c1be.jpg"),
  Recipe("Grilled Cheese", "assets/3187380632_5056654a19_b.jpg"),
  Recipe("Chocolate Chip Cookies", "assets/15992102771_b92f4cc00a_b.jpg"),
  Recipe("Taco Salad", "assets/8533381643_a31a99e8a6_c.jpg"),
  Recipe("Hawaiian Pizza", "assets/15452035777_294cefced5_c.jpg"),
];

assets:
  - assets/

Displaying the list

With the data ready to go, your next step is to create a place for the data to go to.

import 'recipe.dart';
// 4
child: ListView.builder(
  // 5
  itemCount: Recipe.samples.length,
  // 6
  itemBuilder: (BuildContext context, int index) {
    // 7
    return Text(Recipe.samples[index].label);
  },
),

Putting the list into a Card

It’s great that you’re displaying real data now, but this is barely an app. To spice things up a notch, you need to add images to go along with the titles.

Widget buildRecipeCard(Recipe recipe) {
  // 1
  return Card(
    // 2
      child: Column(
        // 3
        children: <Widget>[
          // 4
          Image(image: AssetImage(recipe.imageUrl)),
          // 5
          Text(recipe.label),
        ],
      ),
  );
}
return buildRecipeCard(Recipe.samples[index]);

Looking at the widget tree

Now’s a good time to think about the widget tree of the overall app. Do you remember that it started with RecipeApp from main()?

Making it look nice

The default cards look okay, but they’re not as nice as they could be. With a few added extras, you can spiffy the card up. These include wrapping widgets in layout widgets like Padding or specifying additional styling parameters.

Widget buildRecipeCard(Recipe recipe) {
  return Card(
    // 1
    elevation: 2.0,
    // 2
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10.0)),
    // 3
    child: Padding(
      padding: const EdgeInsets.all(16.0),
      // 4
      child: Column(
        children: <Widget>[
          Image(image: AssetImage(recipe.imageUrl)),
          // 5
          SizedBox(
            height: 14.0,
          ),
          // 6
          Text(
            recipe.label,
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.w700,
              fontFamily: "Palatino",
            ),
          )
        ],
      ),
    ),
  );
}

Adding a recipe detail page

You now have a pretty list, but the app isn’t interactive yet. What would make it great is to show the user details about a recipe when they tap the card. You’ll start implementing this by making the card react to a tap.

Making a tap response

Inside _MyHomePageState, locate ListView.builder(). Replace the itemBuilder return statement with the following:

// 7
return GestureDetector(
  // 8
  onTap: () {
    // 9
    Navigator.push(context,
      MaterialPageRoute(
        builder: (context) {
        // 10
        return Text("Detail page");
      },
    ),
   );
  },
  // 11
  child: buildRecipeCard(Recipe.samples[index]),
);

Creating an actual target page

The resulting page is obviously just a placeholder. Not only is it ugly, but because it doesn’t have all the normal page trappings, the user is now stuck here, at least on iOS devices without a back button. But don’t worry, you can fix that!

import 'package:flutter/material.dart';
import 'recipe.dart';

class RecipeDetail extends StatefulWidget {
  final Recipe recipe;

  RecipeDetail(this.recipe);

  @override
  _RecipeDetailState createState() {
    return _RecipeDetailState(recipe);
  }
}
class _RecipeDetailState extends State {
  final Recipe recipe;

  _RecipeDetailState(this.recipe);

  @override
  Widget build(BuildContext context) {
    // 1
    return Scaffold(
      appBar: AppBar(
        title: Text(recipe.label),
      ),
      // 2
      body: SafeArea(
        // 3
        child: Column(
          children: <Widget>[
            // 4
            Container(
              height: 300,
              width: double.infinity,
              child: Image(image: AssetImage(recipe.imageUrl)),
            ),
            // 5
            SizedBox(
              height: 4,
            ),
            // 6
            Text(
              recipe.label,
              style: TextStyle(fontSize: 18),
            ),
          ],
        ),
      ),
    );
  }
}
import 'recipe_detail.dart';
return RecipeDetail(Recipe.samples[index]);

Adding ingredients

To complete the detail page, you’ll need to add additional details to the Recipe class. Before you can do that, you have to add an ingredient list to the recipes.

class Ingredient {
  double quantity;
  String measure;
  String name;

  Ingredient(this.quantity, this.measure, this.name);
}
int servings;
List<Ingredient> ingredients;
Recipe(this.label, this.imageUrl);
Recipe(this.label, this.imageUrl, this.servings, this.ingredients);

static List<Recipe> samples = [
  Recipe(
    "Spaghetti and Meatballs",
    "assets/2126711929_ef763de2b3_w.jpg",
    4,
    [
      Ingredient(1, "box", "Spaghetti"),
      Ingredient(4, "", "Frozen Meatballs"),
      Ingredient(0.5, "jar", "sauce"),
    ],
  ),
  Recipe(
    "Tomato Soup",
    "assets/27729023535_a57606c1be.jpg",
    2,
    [
      Ingredient(1, "can", "Tomato Soup"),
    ],
  ),
  Recipe(
    "Grilled Cheese",
    "assets/3187380632_5056654a19_b.jpg",
    1,
    [
      Ingredient(2, "slices", "Cheese"),
      Ingredient(2, "slices", "Bread"),
    ],
  ),
  Recipe(
    "Chocolate Chip Cookies",
    "assets/15992102771_b92f4cc00a_b.jpg",
    24,
    [
      Ingredient(4, "cups", "flour"),
      Ingredient(2, "cups", "sugar"),
      Ingredient(0.5, "cups", "chocolate chips"),
    ],
  ),
  Recipe(
    "Taco Salad",
    "assets/8533381643_a31a99e8a6_c.jpg",
    1,
    [
      Ingredient(4, "oz", "nachos"),
      Ingredient(3, "oz", "taco meat"),
      Ingredient(0.5, "cup", "cheese"),
      Ingredient(0.25, "cup", "chopped tomatoes"),
    ],
  ),
  Recipe(
    "Hawaiian Pizza",
    "assets/15452035777_294cefced5_c.jpg",
    4,
    [
      Ingredient(1, "item", "pizza"),
      Ingredient(1, "cup", "pineapple"),
      Ingredient(8, "oz", "ham"),
    ],
  ),
];

Showing the ingredients

A recipe doesn’t do much good without the ingredients. Now, you’re ready to add a widget to display them.

// 7
Expanded(
  // 8
  child: ListView.builder(
    padding: const EdgeInsets.all(7.0),
    itemCount: recipe.ingredients.length,
    itemBuilder: (BuildContext context, int index) {
      final ingredient = recipe.ingredients[index];
      // 9
      return Text(
          "${ingredient.quantity} ${ingredient.measure} ${ingredient.name}");
    },
  ),
),

Adding a serving slider

You’re currently showing the ingredients for a suggested serving. Wouldn’t it be great if you could change the desired quantity and have the amount of ingredients update automatically?

int _sliderVal = 1;
Slider(
  // 10
  min: 1,
  max: 10,
  divisions: 10,
  // 11
  label: "${_sliderVal * recipe.servings} servings",
  // 12
  value: _sliderVal.toDouble(),
  // 13
  onChanged: (newValue) {
    setState(() {
      _sliderVal = newValue.round();
    });
  },
  // 14
  activeColor: Colors.green,
  inactiveColor: Colors.black,
),

Updating the recipe

It’s great to see the changed value reflected in the slider, but right now, it doesn’t affect the recipe itself.

return Text("${ingredient.quantity * _sliderVal} ${ingredient.measure} ${ingredient.name}");

Key points

  • Build a new app with flutter create.
  • Use widgets to compose a screen with controls and layout.
  • Use widget parameters for styling.
  • A MaterialApp widget specifies the app, and Scaffold specifies the high-level structure of a given screen.
  • State allows for interactive widgets.
  • When state changes, you usually need to Hot Restart the app instead of Hot Reload. In some case, you may also need to rebuild and restart the app entirely.

Where to go from here?

Congratulations, you’ve written your first app!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.