pesto_demo.dart 25.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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';

7
class PestoDemo extends StatelessWidget {
8
  const PestoDemo({ super.key });
9 10 11 12

  static const String routeName = '/pesto';

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

16

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

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

25
final ThemeData _kTheme = ThemeData(
26
  appBarTheme: const AppBarTheme(foregroundColor: Colors.white, backgroundColor: Colors.teal),
27
  brightness: Brightness.light,
28
  floatingActionButtonTheme: const FloatingActionButtonThemeData(foregroundColor: Colors.white),
29 30
);

31
class PestoHome extends StatelessWidget {
32
  const PestoHome({super.key});
33

34 35
  @override
  Widget build(BuildContext context) {
36
    return const RecipeGridPage(recipes: kPestoRecipes);
37 38 39 40
  }
}

class PestoFavorites extends StatelessWidget {
41
  const PestoFavorites({super.key});
42

43 44
  @override
  Widget build(BuildContext context) {
45
    return RecipeGridPage(recipes: _favoriteRecipes.toList());
46 47
  }
}
48

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

63 64
// Displays a grid of recipe cards.
class RecipeGridPage extends StatefulWidget {
65
  const RecipeGridPage({ super.key, this.recipes });
66

67
  final List<Recipe?>? recipes;
68 69

  @override
70
  State<RecipeGridPage> createState() => _RecipeGridPageState();
71 72
}

73
class _RecipeGridPageState extends State<RecipeGridPage> {
74 75 76

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

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

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

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

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

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

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

  @override
194
  State<PestoLogo> createState() => _PestoLogoState();
195 196 197 198 199 200 201 202 203
}

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);
204
  final RectTween _textRectTween = RectTween(
Dan Field's avatar
Dan Field committed
205 206
    begin: const Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
    end: const Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight),
207
  );
208
  final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
209
  final RectTween _imageRectTween = RectTween(
Dan Field's avatar
Dan Field committed
210 211
    begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
    end: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
212
  );
213 214 215

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

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

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

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

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

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

315
  final Recipe? recipe;
316 317

  @override
318
  State<RecipePage> createState() => _RecipePageState();
319 320
}

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

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

326 327
  @override
  Widget build(BuildContext context) {
328 329 330 331
    // 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);
332
    final Size screenSize = MediaQuery.of(context).size;
333
    final bool fullWidth = screenSize.width < _kRecipePageMaxWidth;
334
    final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
335 336
    return Scaffold(
      body: Stack(
337
        children: <Widget>[
338
          Positioned(
339 340 341 342
            top: 0.0,
            left: 0.0,
            right: 0.0,
            height: appBarHeight + _kFabHalfSize,
343
            child: Hero(
344
              tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe!.imagePath}',
345
              child: Image.asset(
346 347
                widget.recipe!.imagePath!,
                package: widget.recipe!.imagePackage,
348
                fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
349
              ),
350 351
            ),
          ),
352
          CustomScrollView(
353
            slivers: <Widget>[
354
              SliverAppBar(
355 356 357
                expandedHeight: appBarHeight - _kFabHalfSize,
                backgroundColor: Colors.transparent,
                actions: <Widget>[
358
                  PopupMenuButton<String>(
359
                    onSelected: (String item) { },
360 361 362 363 364 365 366 367
                    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'),
                    ],
                  ),
                ],
368
                flexibleSpace: const FlexibleSpaceBar(
369 370 371
                  background: DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
372
                        begin: Alignment.topCenter,
373 374
                        end: Alignment(0.0, -0.2),
                        colors: <Color>[Color(0x60000000), Color(0x00000000)],
375 376 377 378 379
                      ),
                    ),
                  ),
                ),
              ),
380 381
              SliverToBoxAdapter(
                child: Stack(
382
                  children: <Widget>[
383
                    Container(
384
                      padding: const EdgeInsets.only(top: _kFabHalfSize),
385
                      width: fullWidth ? null : _kRecipePageMaxWidth,
386
                      child: RecipeSheet(recipe: widget.recipe),
387
                    ),
388
                    Positioned(
389
                      right: 16.0,
390
                      child: FloatingActionButton(
391
                        backgroundColor: Colors.redAccent,
392
                        onPressed: _toggleFavorite,
393
                        child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
394 395 396
                      ),
                    ),
                  ],
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
      if (_favoriteRecipes.contains(widget.recipe)) {
423
        _favoriteRecipes.remove(widget.recipe);
424
      } else {
425
        _favoriteRecipes.add(widget.recipe);
426
      }
427 428 429 430
    });
  }
}

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

435 436 437
  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);
438
  final TextStyle itemAmountStyle = PestoStyle(fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0/15.0);
439
  final TextStyle headingStyle = const PestoStyle(fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0/15.0);
440

441
  final Recipe? recipe;
442 443 444

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

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

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

544 545 546 547 548 549 550 551 552
  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;
553 554 555 556 557
}

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

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

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

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

569 570
const List<Recipe> kPestoRecipes = <Recipe>[
  Recipe(
571
    name: 'Roasted Chicken',
572
    author: 'Peter Carlsson',
573
    ingredientsImagePath: 'food/icons/main.png',
574
    ingredientsImagePackage: _kGalleryAssetsPackage,
575 576
    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',
577
    imagePackage: _kGalleryAssetsPackage,
578
    ingredients: <RecipeIngredient>[
579 580 581 582
      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'),
583
      RecipeIngredient(amount: '1 tsp', description: 'Salt'),
584
    ],
585
    steps: <RecipeStep>[
586 587
      RecipeStep(duration: '1 min', description: 'Put in oven'),
      RecipeStep(duration: '1hr 45 min', description: 'Cook'),
588
    ],
589
  ),
590
  Recipe(
591
    name: 'Chopped Beet Leaves',
592
    author: 'Trevor Hansen',
593
    ingredientsImagePath: 'food/icons/veggie.png',
594
    ingredientsImagePackage: _kGalleryAssetsPackage,
595 596
    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',
597
    imagePackage: _kGalleryAssetsPackage,
598
    ingredients: <RecipeIngredient>[
599
       RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
600
    ],
601
    steps: <RecipeStep>[
602
      RecipeStep(duration: '5 min', description: 'Chop'),
603
    ],
604
  ),
605
  Recipe(
606
    name: 'Pesto Pasta',
607
    author: 'Ali Connors',
608
    ingredientsImagePath: 'food/icons/main.png',
609
    ingredientsImagePackage: _kGalleryAssetsPackage,
610
    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.",
611
    imagePath: 'food/pesto_pasta.png',
612
    imagePackage: _kGalleryAssetsPackage,
613
    ingredients: <RecipeIngredient>[
614 615 616 617 618 619 620 621 622
      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'),
623
      RecipeIngredient(amount: '3 lbs', description: 'Bacon'),
624
    ],
625
    steps: <RecipeStep>[
626
      RecipeStep(duration: '15 min', description: 'Blend'),
627
    ],
628
  ),
629
  Recipe(
630
    name: 'Cherry Pie',
631
    author: 'Sandra Adams',
632
    ingredientsImagePath: 'food/icons/main.png',
633
    ingredientsImagePackage: _kGalleryAssetsPackage,
634
    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.",
635
    imagePath: 'food/cherry_pie.png',
636
    imagePackage: _kGalleryAssetsPackage,
637
    ingredients: <RecipeIngredient>[
638 639 640 641 642
      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'),
643
    ],
644
    steps: <RecipeStep>[
645 646
      RecipeStep(duration: '15 min', description: 'Mix'),
      RecipeStep(duration: '1hr 30 min', description: 'Bake'),
647
    ],
648
  ),
649
  Recipe(
650
    name: 'Spinach Salad',
651
    author: 'Peter Carlsson',
652
    ingredientsImagePath: 'food/icons/spicy.png',
653
    ingredientsImagePackage: _kGalleryAssetsPackage,
654
    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.",
655
    imagePath: 'food/spinach_onion_salad.png',
656
    imagePackage: _kGalleryAssetsPackage,
657
    ingredients: <RecipeIngredient>[
658 659
      RecipeIngredient(amount: '4 cups', description: 'Spinach'),
      RecipeIngredient(amount: '1 cup', description: 'Sliced onion'),
660
    ],
661
    steps: <RecipeStep>[
662
      RecipeStep(duration: '5 min', description: 'Mix'),
663
    ],
664
  ),
665
  Recipe(
666
    name: 'Butternut Squash Soup',
667
    author: 'Ali Connors',
668
    ingredientsImagePath: 'food/icons/healthy.png',
669
    ingredientsImagePackage: _kGalleryAssetsPackage,
670 671
    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',
672
    imagePackage: _kGalleryAssetsPackage,
673
    ingredients: <RecipeIngredient>[
674 675 676 677 678 679 680 681
      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'),
682
    ],
683
    steps: <RecipeStep>[
684 685
      RecipeStep(duration: '10 min', description: 'Prep vegetables'),
      RecipeStep(duration: '5 min', description: 'Stir'),
686
      RecipeStep(duration: '1 hr 10 min', description: 'Cook'),
687
    ],
688
  ),
689
  Recipe(
690
    name: 'Spanakopita',
691
    author: 'Trevor Hansen',
692
    ingredientsImagePath: 'food/icons/quick.png',
693
    ingredientsImagePackage: _kGalleryAssetsPackage,
694
    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.",
695
    imagePath: 'food/spanakopita.png',
696
    imagePackage: _kGalleryAssetsPackage,
697
    ingredients: <RecipeIngredient>[
698 699 700 701 702 703
      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'),
704
    ],
705
    steps: <RecipeStep>[
706 707 708
      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.'),
709
      RecipeStep(duration: '40 min', description: 'Bake'),
710
    ],
711 712
  ),
];