pesto_demo.dart 25 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart';
7

8
class PestoDemo extends StatelessWidget {
9
  const PestoDemo({ Key? key }) : super(key: key);
10 11 12 13

  static const String routeName = '/pesto';

  @override
14
  Widget build(BuildContext context) => PestoHome();
15 16
}

17

18
const String _kSmallLogoImage = 'logos/pesto/logo_small.png';
19
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
20
const double _kAppBarHeight = 128.0;
21
const double _kFabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size
22 23
const double _kRecipePageMaxWidth = 500.0;

24
final Set<Recipe?> _favoriteRecipes = <Recipe?>{};
25

26
final ThemeData _kTheme = ThemeData(
27
  brightness: Brightness.light,
28
  primarySwatch: Colors.teal,
29
  accentColor: Colors.redAccent,
30 31
);

32 33 34
class PestoHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
35
    return const RecipeGridPage(recipes: kPestoRecipes);
36 37 38 39 40 41
  }
}

class PestoFavorites extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
42
    return RecipeGridPage(recipes: _favoriteRecipes.toList());
43 44
  }
}
45

46 47
class PestoStyle extends TextStyle {
  const PestoStyle({
48
    double fontSize = 12.0,
49
    FontWeight? fontWeight,
50
    Color color = Colors.black87,
51 52
    double? letterSpacing,
    double? height,
53
  }) : super(
54
    inherit: false,
55
    color: color,
56
    fontFamily: 'Raleway',
57 58 59
    fontSize: fontSize,
    fontWeight: fontWeight,
    textBaseline: TextBaseline.alphabetic,
60 61
    letterSpacing: letterSpacing,
    height: height,
62 63 64
  );
}

65 66
// Displays a grid of recipe cards.
class RecipeGridPage extends StatefulWidget {
67
  const RecipeGridPage({ Key? key, this.recipes }) : super(key: key);
68

69
  final List<Recipe?>? recipes;
70 71

  @override
72
  _RecipeGridPageState createState() => _RecipeGridPageState();
73 74
}

75
class _RecipeGridPageState extends State<RecipeGridPage> {
76 77 78

  @override
  Widget build(BuildContext context) {
79
    final double statusBarHeight = MediaQuery.of(context).padding.top;
80
    return Theme(
81
      data: _kTheme.copyWith(platform: Theme.of(context).platform),
82 83
      child: Scaffold(
        floatingActionButton: FloatingActionButton(
84
          child: const Icon(Icons.edit),
85
          onPressed: () {
86
            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
87
              content: Text('Not supported.'),
88
            ));
89
          },
90
        ),
91
        body: CustomScrollView(
92
          semanticChildCount: widget.recipes!.length,
93 94 95 96 97
          slivers: <Widget>[
            _buildAppBar(context, statusBarHeight),
            _buildBody(context, statusBarHeight),
          ],
        ),
98
      ),
99 100 101
    );
  }

102
  Widget _buildAppBar(BuildContext context, double statusBarHeight) {
103
    return SliverAppBar(
104
      pinned: true,
105
      expandedHeight: _kAppBarHeight,
106
      actions: <Widget>[
107
        IconButton(
108
          icon: const Icon(Icons.search),
109 110
          tooltip: 'Search',
          onPressed: () {
111
            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
112
              content: Text('Not supported.'),
113
            ));
114 115
          },
        ),
116
      ],
117
      flexibleSpace: LayoutBuilder(
118 119
        builder: (BuildContext context, BoxConstraints constraints) {
          final Size size = constraints.biggest;
120
          final double appBarHeight = size.height - statusBarHeight;
121
          final double t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight);
122
          final double extraPadding = Tween<double>(begin: 10.0, end: 24.0).transform(t);
123
          final double logoHeight = appBarHeight - 1.5 * extraPadding;
124 125
          return Padding(
            padding: EdgeInsets.only(
126
              top: statusBarHeight + 0.5 * extraPadding,
127
              bottom: extraPadding,
128
            ),
129
            child: Center(
130
              child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0)),
131
            ),
132
          );
133 134
        },
      ),
135 136 137
    );
  }

138
  Widget _buildBody(BuildContext context, double statusBarHeight) {
139
    final EdgeInsets mediaPadding = MediaQuery.of(context).padding;
140
    final EdgeInsets padding = EdgeInsets.only(
141 142 143
      top: 8.0,
      left: 8.0 + mediaPadding.left,
      right: 8.0 + mediaPadding.right,
144
      bottom: 8.0,
145
    );
146
    return SliverPadding(
147
      padding: padding,
148
      sliver: SliverGrid(
149
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
150 151 152 153
          maxCrossAxisExtent: _kRecipePageMaxWidth,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
        ),
154
        delegate: SliverChildBuilderDelegate(
155
          (BuildContext context, int index) {
156
            final Recipe? recipe = widget.recipes![index];
157
            return RecipeCard(
158 159 160
              recipe: recipe,
              onTap: () { showRecipePage(context, recipe); },
            );
Ian Hickson's avatar
Ian Hickson committed
161
          },
162
          childCount: widget.recipes!.length,
163
        ),
164 165 166 167
      ),
    );
  }

168
  void showFavoritesPage(BuildContext context) {
169
    Navigator.push(context, MaterialPageRoute<void>(
170
      settings: const RouteSettings(name: '/pesto/favorites'),
171
      builder: (BuildContext context) => PestoFavorites(),
172 173 174
    ));
  }

175
  void showRecipePage(BuildContext context, Recipe? recipe) {
176
    Navigator.push(context, MaterialPageRoute<void>(
177
      settings: const RouteSettings(name: '/pesto/recipe'),
178
      builder: (BuildContext context) {
179
        return Theme(
180
          data: _kTheme.copyWith(platform: Theme.of(context).platform),
181
          child: RecipePage(recipe: recipe),
182
        );
183
      },
184 185
    ));
  }
186 187 188
}

class PestoLogo extends StatefulWidget {
189
  const PestoLogo({this.height, this.t});
190

191 192
  final double? height;
  final double? t;
193 194

  @override
195
  _PestoLogoState createState() => _PestoLogoState();
196 197 198 199 200 201 202 203 204
}

class _PestoLogoState extends State<PestoLogo> {
  // Native sizes for logo and its image/text components.
  static const double kLogoHeight = 162.0;
  static const double kLogoWidth = 220.0;
  static const double kImageHeight = 108.0;
  static const double kTextHeight = 48.0;
  final TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0);
205
  final RectTween _textRectTween = RectTween(
Dan Field's avatar
Dan Field committed
206 207
    begin: const Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
    end: const Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight),
208
  );
209
  final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
210
  final RectTween _imageRectTween = RectTween(
Dan Field's avatar
Dan Field committed
211 212
    begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
    end: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
213
  );
214 215 216

  @override
  Widget build(BuildContext context) {
217
    return Semantics(
218
      namesRoute: true,
219
      child: Transform(
220
        transform: Matrix4.identity()..scale(widget.height! / kLogoHeight),
221
        alignment: Alignment.topCenter,
222
        child: SizedBox(
223
          width: kLogoWidth,
224
          child: Stack(
225
            clipBehavior: Clip.none,
226
            children: <Widget>[
227
              Positioned.fromRect(
228
                rect: _imageRectTween.lerp(widget.t!)!,
229
                child: Image.asset(
230 231 232 233
                  _kSmallLogoImage,
                  package: _kGalleryAssetsPackage,
                  fit: BoxFit.contain,
                ),
234
              ),
235
              Positioned.fromRect(
236
                rect: _textRectTween.lerp(widget.t!)!,
237
                child: Opacity(
238
                  opacity: _textOpacity.transform(widget.t!),
239
                  child: Text('PESTO', style: titleStyle, textAlign: TextAlign.center),
240
                ),
241
              ),
242 243
            ],
          ),
244 245
        ),
      ),
246 247
    );
  }
248 249
}

250 251
// A card with the recipe's image, author, and title.
class RecipeCard extends StatelessWidget {
252
  const RecipeCard({ Key? key, this.recipe, this.onTap }) : super(key: key);
253

254 255
  final Recipe? recipe;
  final VoidCallback? onTap;
256

257 258 259
  TextStyle get titleStyle => const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
  TextStyle get authorStyle => const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);

260 261
  @override
  Widget build(BuildContext context) {
262
    return GestureDetector(
263
      onTap: onTap,
264 265
      child: Card(
        child: Column(
266 267
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
268
            Hero(
269
              tag: 'packages/$_kGalleryAssetsPackage/${recipe!.imagePath}',
270 271 272
              child: AspectRatio(
                aspectRatio: 4.0 / 3.0,
                child: Image.asset(
273 274
                  recipe!.imagePath!,
                  package: recipe!.imagePackage,
275
                  fit: BoxFit.cover,
276
                  semanticLabel: recipe!.name,
277
                ),
278
              ),
279
            ),
280 281
            Expanded(
              child: Row(
282
                children: <Widget>[
283
                  Padding(
284
                    padding: const EdgeInsets.all(16.0),
285
                    child: Image.asset(
286 287
                      recipe!.ingredientsImagePath!,
                      package: recipe!.ingredientsImagePackage,
288 289
                      width: 48.0,
                      height: 48.0,
290
                    ),
291
                  ),
292 293
                  Expanded(
                    child: Column(
294 295 296
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
297 298
                        Text(recipe!.name!, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis),
                        Text(recipe!.author!, style: authorStyle),
299
                      ],
300
                    ),
301 302
                  ),
                ],
303
              ),
304 305
            ),
          ],
306 307
        ),
      ),
308 309 310 311
    );
  }
}

312 313
// Displays one recipe. Includes the recipe sheet with a background image.
class RecipePage extends StatefulWidget {
314
  const RecipePage({ Key? key, this.recipe }) : super(key: key);
315

316
  final Recipe? recipe;
317 318

  @override
319
  _RecipePageState createState() => _RecipePageState();
320 321
}

322
class _RecipePageState extends State<RecipePage> {
323
  final TextStyle menuItemStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
324

325 326
  double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;

327 328
  @override
  Widget build(BuildContext context) {
329 330 331 332
    // The full page content with the recipe's image behind it. This
    // adjusts based on the size of the screen. If the recipe sheet touches
    // the edge of the screen, use a slightly different layout.
    final double appBarHeight = _getAppBarHeight(context);
333
    final Size screenSize = MediaQuery.of(context).size;
334
    final bool fullWidth = screenSize.width < _kRecipePageMaxWidth;
335
    final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
336 337
    return Scaffold(
      body: Stack(
338
        children: <Widget>[
339
          Positioned(
340 341 342 343
            top: 0.0,
            left: 0.0,
            right: 0.0,
            height: appBarHeight + _kFabHalfSize,
344
            child: Hero(
345
              tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe!.imagePath}',
346
              child: Image.asset(
347 348
                widget.recipe!.imagePath!,
                package: widget.recipe!.imagePackage,
349
                fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
350
              ),
351 352
            ),
          ),
353
          CustomScrollView(
354
            slivers: <Widget>[
355
              SliverAppBar(
356 357 358
                expandedHeight: appBarHeight - _kFabHalfSize,
                backgroundColor: Colors.transparent,
                actions: <Widget>[
359
                  PopupMenuButton<String>(
360
                    onSelected: (String item) { },
361 362 363 364 365 366 367 368
                    itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
                      _buildMenuItem(Icons.share, 'Tweet recipe'),
                      _buildMenuItem(Icons.email, 'Email recipe'),
                      _buildMenuItem(Icons.message, 'Message recipe'),
                      _buildMenuItem(Icons.people, 'Share on Facebook'),
                    ],
                  ),
                ],
369
                flexibleSpace: const FlexibleSpaceBar(
370 371 372 373 374 375
                  background: DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment(0.0, -1.0),
                        end: Alignment(0.0, -0.2),
                        colors: <Color>[Color(0x60000000), Color(0x00000000)],
376 377 378 379 380
                      ),
                    ),
                  ),
                ),
              ),
381 382
              SliverToBoxAdapter(
                child: Stack(
383
                  children: <Widget>[
384
                    Container(
385
                      padding: const EdgeInsets.only(top: _kFabHalfSize),
386
                      width: fullWidth ? null : _kRecipePageMaxWidth,
387
                      child: RecipeSheet(recipe: widget.recipe),
388
                    ),
389
                    Positioned(
390
                      right: 16.0,
391 392
                      child: FloatingActionButton(
                        child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
393 394 395 396
                        onPressed: _toggleFavorite,
                      ),
                    ),
                  ],
397
                ),
398
              ),
399
            ],
400
          ),
401 402
        ],
      ),
403 404 405 406
    );
  }

  PopupMenuItem<String> _buildMenuItem(IconData icon, String label) {
407 408
    return PopupMenuItem<String>(
      child: Row(
409
        children: <Widget>[
410
          Padding(
411
            padding: const EdgeInsets.only(right: 24.0),
412
            child: Icon(icon, color: Colors.black54),
413
          ),
414
          Text(label, style: menuItemStyle),
415 416
        ],
      ),
417 418 419 420 421
    );
  }

  void _toggleFavorite() {
    setState(() {
422 423
      if (_favoriteRecipes.contains(widget.recipe))
        _favoriteRecipes.remove(widget.recipe);
424
      else
425
        _favoriteRecipes.add(widget.recipe);
426 427 428 429
    });
  }
}

430 431
/// Displays the recipe's name and instructions.
class RecipeSheet extends StatelessWidget {
432
  RecipeSheet({ Key? key, this.recipe }) : super(key: key);
433

434 435 436
  final TextStyle titleStyle = const PestoStyle(fontSize: 34.0);
  final TextStyle descriptionStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
  final TextStyle itemStyle = const PestoStyle(fontSize: 15.0, height: 24.0/15.0);
437
  final TextStyle itemAmountStyle = PestoStyle(fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0/15.0);
438
  final TextStyle headingStyle = const PestoStyle(fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0/15.0);
439

440
  final Recipe? recipe;
441 442 443

  @override
  Widget build(BuildContext context) {
444 445
    return Material(
      child: SafeArea(
446 447
        top: false,
        bottom: false,
448
        child: Padding(
449
          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
450
          child: Table(
451
            columnWidths: const <int, TableColumnWidth>{
452
              0: FixedColumnWidth(64.0),
453 454
            },
            children: <TableRow>[
455
              TableRow(
456
                children: <Widget>[
457
                  TableCell(
458
                    verticalAlignment: TableCellVerticalAlignment.middle,
459
                    child: Image.asset(
460 461
                      recipe!.ingredientsImagePath!,
                      package: recipe!.ingredientsImagePackage,
462 463 464
                      width: 32.0,
                      height: 32.0,
                      alignment: Alignment.centerLeft,
465 466
                      fit: BoxFit.scaleDown,
                    ),
467
                  ),
468
                  TableCell(
469
                    verticalAlignment: TableCellVerticalAlignment.middle,
470
                    child: Text(recipe!.name!, style: titleStyle),
471 472 473
                  ),
                ]
              ),
474
              TableRow(
475 476
                children: <Widget>[
                  const SizedBox(),
477
                  Padding(
478
                    padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
479
                    child: Text(recipe!.description!, style: descriptionStyle),
480 481 482
                  ),
                ]
              ),
483
              TableRow(
484 485
                children: <Widget>[
                  const SizedBox(),
486
                  Padding(
487
                    padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
488
                    child: Text('Ingredients', style: headingStyle),
489 490 491
                  ),
                ]
              ),
492 493
              ...recipe!.ingredients!.map<TableRow>((RecipeIngredient ingredient) {
                return _buildItemRow(ingredient.amount!, ingredient.description!);
494
              }),
495
              TableRow(
496 497
                children: <Widget>[
                  const SizedBox(),
498
                  Padding(
499
                    padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
500
                    child: Text('Steps', style: headingStyle),
501 502
                  ),
                ]
503
              ),
504 505
              ...recipe!.steps!.map<TableRow>((RecipeStep step) {
                return _buildItemRow(step.duration ?? '', step.description!);
506 507
              }),
            ],
508
          ),
509 510
        ),
      ),
511 512 513 514
    );
  }

  TableRow _buildItemRow(String left, String right) {
515
    return TableRow(
516
      children: <Widget>[
517
        Padding(
518
          padding: const EdgeInsets.symmetric(vertical: 4.0),
519
          child: Text(left, style: itemAmountStyle),
520
        ),
521
        Padding(
522
          padding: const EdgeInsets.symmetric(vertical: 4.0),
523
          child: Text(right, style: itemStyle),
524 525
        ),
      ],
526 527 528 529 530 531 532 533 534 535
    );
  }
}

class Recipe {
  const Recipe({
    this.name,
    this.author,
    this.description,
    this.imagePath,
536
    this.imagePackage,
537
    this.ingredientsImagePath,
538
    this.ingredientsImagePackage,
539
    this.ingredients,
540
    this.steps,
541 542
  });

543 544 545 546 547 548 549 550 551
  final String? name;
  final String? author;
  final String? description;
  final String? imagePath;
  final String? imagePackage;
  final String? ingredientsImagePath;
  final String? ingredientsImagePackage;
  final List<RecipeIngredient>? ingredients;
  final List<RecipeStep>? steps;
552 553 554 555 556
}

class RecipeIngredient {
  const RecipeIngredient({this.amount, this.description});

557 558
  final String? amount;
  final String? description;
559 560 561 562 563
}

class RecipeStep {
  const RecipeStep({this.duration, this.description});

564 565
  final String? duration;
  final String? description;
566 567
}

568 569
const List<Recipe> kPestoRecipes = <Recipe>[
  Recipe(
570
    name: 'Roasted Chicken',
571
    author: 'Peter Carlsson',
572
    ingredientsImagePath: 'food/icons/main.png',
573
    ingredientsImagePackage: _kGalleryAssetsPackage,
574 575
    description: 'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.',
    imagePath: 'food/roasted_chicken.png',
576
    imagePackage: _kGalleryAssetsPackage,
577
    ingredients: <RecipeIngredient>[
578 579 580 581
      RecipeIngredient(amount: '1 whole', description: 'Chicken'),
      RecipeIngredient(amount: '1/2 cup', description: 'Butter'),
      RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'),
      RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'),
582
      RecipeIngredient(amount: '1 tsp', description: 'Salt'),
583
    ],
584
    steps: <RecipeStep>[
585 586
      RecipeStep(duration: '1 min', description: 'Put in oven'),
      RecipeStep(duration: '1hr 45 min', description: 'Cook'),
587
    ],
588
  ),
589
  Recipe(
590
    name: 'Chopped Beet Leaves',
591
    author: 'Trevor Hansen',
592
    ingredientsImagePath: 'food/icons/veggie.png',
593
    ingredientsImagePackage: _kGalleryAssetsPackage,
594 595
    description: 'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.',
    imagePath: 'food/chopped_beet_leaves.png',
596
    imagePackage: _kGalleryAssetsPackage,
597
    ingredients: <RecipeIngredient>[
598
       RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
599
    ],
600
    steps: <RecipeStep>[
601
      RecipeStep(duration: '5 min', description: 'Chop'),
602
    ],
603
  ),
604
  Recipe(
605
    name: 'Pesto Pasta',
606
    author: 'Ali Connors',
607
    ingredientsImagePath: 'food/icons/main.png',
608
    ingredientsImagePackage: _kGalleryAssetsPackage,
609
    description: "With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you're feeling festive, you can add bacon to taste.",
610
    imagePath: 'food/pesto_pasta.png',
611
    imagePackage: _kGalleryAssetsPackage,
612
    ingredients: <RecipeIngredient>[
613 614 615 616 617 618 619 620 621
      RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'),
      RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'),
      RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'),
      RecipeIngredient(amount: '1/2 cup', description: 'Extra virgin olive oil'),
      RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'),
      RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'),
      RecipeIngredient(amount: '3 cloves', description: 'Garlic'),
      RecipeIngredient(amount: '1/4 tsp', description: 'Salt'),
      RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'),
622
      RecipeIngredient(amount: '3 lbs', description: 'Bacon'),
623
    ],
624
    steps: <RecipeStep>[
625
      RecipeStep(duration: '15 min', description: 'Blend'),
626
    ],
627
  ),
628
  Recipe(
629
    name: 'Cherry Pie',
630
    author: 'Sandra Adams',
631
    ingredientsImagePath: 'food/icons/main.png',
632
    ingredientsImagePackage: _kGalleryAssetsPackage,
633
    description: "Sometimes when you're craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.",
634
    imagePath: 'food/cherry_pie.png',
635
    imagePackage: _kGalleryAssetsPackage,
636
    ingredients: <RecipeIngredient>[
637 638 639 640 641
      RecipeIngredient(amount: '1', description: 'Pie crust'),
      RecipeIngredient(amount: '4 cups', description: 'Fresh or frozen cherries'),
      RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'),
      RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'),
      RecipeIngredient(amount: '1½ tbsp', description: 'Butter'),
642
    ],
643
    steps: <RecipeStep>[
644 645
      RecipeStep(duration: '15 min', description: 'Mix'),
      RecipeStep(duration: '1hr 30 min', description: 'Bake'),
646
    ],
647
  ),
648
  Recipe(
649
    name: 'Spinach Salad',
650
    author: 'Peter Carlsson',
651
    ingredientsImagePath: 'food/icons/spicy.png',
652
    ingredientsImagePackage: _kGalleryAssetsPackage,
653
    description: "Everyone's favorite leafy green is back. Paired with fresh sliced onion, it's ready to tackle any dish, whether it be a salad or an egg scramble.",
654
    imagePath: 'food/spinach_onion_salad.png',
655
    imagePackage: _kGalleryAssetsPackage,
656
    ingredients: <RecipeIngredient>[
657 658
      RecipeIngredient(amount: '4 cups', description: 'Spinach'),
      RecipeIngredient(amount: '1 cup', description: 'Sliced onion'),
659
    ],
660
    steps: <RecipeStep>[
661
      RecipeStep(duration: '5 min', description: 'Mix'),
662
    ],
663
  ),
664
  Recipe(
665
    name: 'Butternut Squash Soup',
666
    author: 'Ali Connors',
667
    ingredientsImagePath: 'food/icons/healthy.png',
668
    ingredientsImagePackage: _kGalleryAssetsPackage,
669 670
    description: 'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.',
    imagePath: 'food/butternut_squash_soup.png',
671
    imagePackage: _kGalleryAssetsPackage,
672
    ingredients: <RecipeIngredient>[
673 674 675 676 677 678 679 680
      RecipeIngredient(amount: '1', description: 'Butternut squash'),
      RecipeIngredient(amount: '4 cups', description: 'Chicken stock'),
      RecipeIngredient(amount: '2', description: 'Potatoes'),
      RecipeIngredient(amount: '1', description: 'Onion'),
      RecipeIngredient(amount: '1', description: 'Carrot'),
      RecipeIngredient(amount: '1', description: 'Celery'),
      RecipeIngredient(amount: '1 tsp', description: 'Salt'),
      RecipeIngredient(amount: '1 tsp', description: 'Pepper'),
681
    ],
682
    steps: <RecipeStep>[
683 684
      RecipeStep(duration: '10 min', description: 'Prep vegetables'),
      RecipeStep(duration: '5 min', description: 'Stir'),
685
      RecipeStep(duration: '1 hr 10 min', description: 'Cook'),
686
    ],
687
  ),
688
  Recipe(
689
    name: 'Spanakopita',
690
    author: 'Trevor Hansen',
691
    ingredientsImagePath: 'food/icons/quick.png',
692
    ingredientsImagePackage: _kGalleryAssetsPackage,
693
    description: "You 'feta' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.",
694
    imagePath: 'food/spanakopita.png',
695
    imagePackage: _kGalleryAssetsPackage,
696
    ingredients: <RecipeIngredient>[
697 698 699 700 701 702
      RecipeIngredient(amount: '1 lb', description: 'Spinach'),
      RecipeIngredient(amount: '½ cup', description: 'Feta cheese'),
      RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'),
      RecipeIngredient(amount: '2', description: 'Eggs'),
      RecipeIngredient(amount: '1', description: 'Onion'),
      RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'),
703
    ],
704
    steps: <RecipeStep>[
705 706 707
      RecipeStep(duration: '5 min', description: 'Sauté vegetables'),
      RecipeStep(duration: '3 min', description: 'Stir vegetables and other filling ingredients'),
      RecipeStep(duration: '10 min', description: 'Fill phyllo squares half-full with filling and fold.'),
708
      RecipeStep(duration: '40 min', description: 'Bake'),
709
    ],
710 711
  ),
];