1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +00:00

Moved veggieseasons out of experimental (#752)

This commit is contained in:
Tushar Ojha
2021-03-08 14:55:15 +05:30
committed by GitHub
parent 51ebc7e0bf
commit 99c18b18d7
136 changed files with 44 additions and 56 deletions

View File

@@ -0,0 +1,350 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/close_button.dart';
import 'package:veggieseasons/widgets/trivia.dart';
class ServingInfoChart extends StatelessWidget {
const ServingInfoChart(this.veggie, this.prefs);
final Veggie veggie;
final Preferences prefs;
// Creates a [Text] widget to display a veggie's "percentage of your daily
// value of this vitamin" data adjusted for the user's preferred calorie
// target.
Widget _buildVitaminText(int standardPercentage, Future<int> targetCalories) {
return FutureBuilder<int>(
future: targetCalories,
builder: (context, snapshot) {
final target = snapshot?.data ?? 2000;
final percent = standardPercentage * 2000 ~/ target;
return Text(
'$percent% DV',
style: CupertinoTheme.of(context).textTheme.textStyle,
textAlign: TextAlign.end,
);
},
);
}
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return Column(
children: [
SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 9,
bottom: 4,
),
child: Text(
'Serving info',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
),
),
Container(
decoration: BoxDecoration(
border: Border.all(color: Styles.servingInfoBorderColor),
),
padding: const EdgeInsets.all(8),
child: Column(
children: [
Table(
children: [
TableRow(
children: [
TableCell(
child: Text(
'Serving size:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: Text(
veggie.servingSize,
textAlign: TextAlign.end,
style: CupertinoTheme.of(context).textTheme.textStyle,
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Calories:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: Text(
'${veggie.caloriesPerServing} kCal',
style: CupertinoTheme.of(context).textTheme.textStyle,
textAlign: TextAlign.end,
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Vitamin A:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: _buildVitaminText(
veggie.vitaminAPercentage,
prefs.desiredCalories,
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Vitamin C:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: _buildVitaminText(
veggie.vitaminCPercentage,
prefs.desiredCalories,
),
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: FutureBuilder(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Text(
'Percent daily values based on a diet of '
'${snapshot?.data ?? '2,000'} calories.',
style: Styles.detailsServingNoteText(themeData),
);
},
),
),
],
),
)
],
);
}
}
class InfoView extends StatelessWidget {
final int id;
const InfoView(this.id);
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final veggie = appState.getVeggie(id);
final themeData = CupertinoTheme.of(context);
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
FutureBuilder<Set<VeggieCategory>>(
future: prefs.preferredCategories,
builder: (context, snapshot) {
return Text(
veggie.categoryName.toUpperCase(),
style: (snapshot.hasData &&
snapshot.data.contains(veggie.category))
? Styles.detailsPreferredCategoryText(themeData)
: themeData.textTheme.textStyle,
);
},
),
Spacer(),
for (Season season in veggie.seasons) ...[
SizedBox(width: 12),
Padding(
padding: Styles.seasonIconPadding[season],
child: Icon(
Styles.seasonIconData[season],
semanticLabel: seasonNames[season],
color: Styles.seasonColors[season],
),
),
],
],
),
SizedBox(height: 8),
Text(
veggie.name,
style: Styles.detailsTitleText(themeData),
),
SizedBox(height: 8),
Text(
veggie.shortDescription,
style: CupertinoTheme.of(context).textTheme.textStyle,
),
ServingInfoChart(veggie, prefs),
SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoSwitch(
value: veggie.isFavorite,
onChanged: (value) {
appState.setFavorite(id, value);
},
),
SizedBox(width: 8),
Text(
'Save to Garden',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
],
),
],
),
);
}
}
class DetailsScreen extends StatefulWidget {
final int id;
final String restorationId;
DetailsScreen({this.id, this.restorationId});
static String show(NavigatorState navigator, int veggieId) {
return navigator.restorablePush<void>(_routeBuilder, arguments: veggieId);
}
static Route<void> _routeBuilder(BuildContext context, Object arguments) {
final veggieId = arguments as int;
return CupertinoPageRoute(
builder: (context) =>
DetailsScreen(id: veggieId, restorationId: 'details'),
fullscreenDialog: true,
);
}
@override
_DetailsScreenState createState() => _DetailsScreenState();
}
class _DetailsScreenState extends State<DetailsScreen> with RestorationMixin {
final RestorableInt _selectedViewIndex = RestorableInt(0);
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(_selectedViewIndex, 'tab');
}
@override
void dispose() {
_selectedViewIndex.dispose();
super.dispose();
}
Widget _buildHeader(BuildContext context, AppState model) {
final veggie = model.getVeggie(widget.id);
return SizedBox(
height: 150,
child: Stack(
children: [
Positioned(
right: 0,
left: 0,
child: Image.asset(
veggie.imageAssetPath,
fit: BoxFit.cover,
semanticLabel: 'A background image of ${veggie.name}',
),
),
Positioned(
top: 16,
left: 16,
child: SafeArea(
child: CloseButton(() {
Navigator.of(context).pop();
}),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
return UnmanagedRestorationScope(
bucket: bucket,
child: CupertinoPageScaffold(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
restorationId: 'list',
children: [
_buildHeader(context, appState),
SizedBox(height: 20),
CupertinoSegmentedControl<int>(
children: {
0: Text(
'Facts & Info',
),
1: Text(
'Trivia',
)
},
groupValue: _selectedViewIndex.value,
onValueChanged: (value) {
setState(() => _selectedViewIndex.value = value);
},
),
_selectedViewIndex.value == 0
? InfoView(widget.id)
: TriviaView(id: widget.id, restorationId: 'trivia'),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/widgets/veggie_headline.dart';
class FavoritesScreen extends StatelessWidget {
FavoritesScreen({this.restorationId, Key key}) : super(key: key);
final String restorationId;
@override
Widget build(BuildContext context) {
return CupertinoTabView(
restorationScopeId: restorationId,
builder: (context) {
final model = Provider.of<AppState>(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('My Garden'),
),
child: Center(
child: model.favoriteVeggies.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'You haven\'t added any favorite veggies to your garden yet.',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
)
: ListView(
restorationId: 'list',
children: [
SizedBox(height: 24),
for (Veggie veggie in model.favoriteVeggies)
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 24),
child: VeggieHeadline(veggie),
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,55 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:veggieseasons/screens/favorites.dart';
import 'package:veggieseasons/screens/list.dart';
import 'package:veggieseasons/screens/search.dart';
import 'package:veggieseasons/screens/settings.dart';
class HomeScreen extends StatelessWidget {
HomeScreen({Key key, this.restorationId}) : super(key: key);
final String restorationId;
@override
Widget build(BuildContext context) {
return RestorationScope(
restorationId: restorationId,
child: CupertinoTabScaffold(
restorationId: 'scaffold',
tabBar: CupertinoTabBar(items: [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.book),
label: 'My Garden',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.search),
label: 'Search',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.settings),
label: 'Settings',
),
]),
tabBuilder: (context, index) {
if (index == 0) {
return ListScreen(restorationId: 'list');
} else if (index == 1) {
return FavoritesScreen(restorationId: 'favorites');
} else if (index == 2) {
return SearchScreen(restorationId: 'search');
} else {
return SettingsScreen(restorationId: 'settings');
}
},
),
);
}
}

View File

@@ -0,0 +1,91 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/veggie_card.dart';
class ListScreen extends StatelessWidget {
ListScreen({this.restorationId, Key key}) : super(key: key);
final String restorationId;
Widget _generateVeggieRow(Veggie veggie, Preferences prefs,
{bool inSeason = true}) {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 24),
child: FutureBuilder<Set<VeggieCategory>>(
future: prefs.preferredCategories,
builder: (context, snapshot) {
final data = snapshot.data ?? <VeggieCategory>{};
return VeggieCard(veggie, inSeason, data.contains(veggie.category));
}),
);
}
@override
Widget build(BuildContext context) {
return CupertinoTabView(
restorationScopeId: restorationId,
builder: (context) {
var dateString = DateFormat('MMMM y').format(DateTime.now());
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final themeData = CupertinoTheme.of(context);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarBrightness: MediaQuery.platformBrightnessOf(context)),
child: SafeArea(
bottom: false,
child: ListView.builder(
restorationId: 'list',
itemCount: appState.allVeggies.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(dateString.toUpperCase(),
style: Styles.minorText(themeData)),
Text('In season today',
style: Styles.headlineText(themeData)),
],
),
);
} else if (index <= appState.availableVeggies.length) {
return _generateVeggieRow(
appState.availableVeggies[index - 1],
prefs,
);
} else if (index <= appState.availableVeggies.length + 1) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Text('Not in season',
style: Styles.headlineText(themeData)),
);
} else {
var relativeIndex =
index - (appState.availableVeggies.length + 2);
return _generateVeggieRow(
appState.unavailableVeggies[relativeIndex], prefs,
inSeason: false);
}
},
),
),
);
},
);
}
}

View File

@@ -0,0 +1,123 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/widgets/veggie_headline.dart';
class SearchScreen extends StatefulWidget {
SearchScreen({this.restorationId, Key key}) : super(key: key);
final String restorationId;
@override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> with RestorationMixin {
final controller = RestorableTextEditingController();
final focusNode = FocusNode();
String terms;
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(controller, 'text');
controller.addListener(_onTextChanged);
terms = controller.value.text;
}
@override
void dispose() {
focusNode.dispose();
controller.dispose();
super.dispose();
}
void _onTextChanged() {
setState(() => terms = controller.value.text);
}
Widget _createSearchBox() {
return Padding(
padding: const EdgeInsets.all(8),
child: CupertinoSearchTextField(
controller: controller.value,
focusNode: focusNode,
),
);
}
Widget _buildSearchResults(List<Veggie> veggies) {
if (veggies.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'No veggies matching your search terms were found.',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
),
);
}
return ListView.builder(
restorationId: 'list',
itemCount: veggies.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return Visibility(
// This invisible and otherwise unnecessary search box is used to
// pad the list entries downward, so none will be underneath the
// real search box when the list is at its top scroll position.
child: _createSearchBox(),
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
);
} else {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 24),
child: VeggieHeadline(veggies[i - 1]),
);
}
},
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<AppState>(context);
return UnmanagedRestorationScope(
bucket: bucket,
child: CupertinoTabView(
restorationScopeId: 'tabview',
builder: (context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarBrightness: MediaQuery.platformBrightnessOf(context),
),
child: SafeArea(
bottom: false,
child: Stack(
children: [
_buildSearchResults(model.searchVeggies(terms)),
_createSearchBox(),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,292 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/settings_group.dart';
import 'package:veggieseasons/widgets/settings_item.dart';
class VeggieCategorySettingsScreen extends StatelessWidget {
VeggieCategorySettingsScreen({Key key, this.restorationId}) : super(key: key);
final String restorationId;
static String show(NavigatorState navigator) {
return navigator.restorablePush(_routeBuilder);
}
static Route<void> _routeBuilder(BuildContext context, Object argument) {
return CupertinoPageRoute(
builder: (context) =>
VeggieCategorySettingsScreen(restorationId: 'category'),
title: 'Preferred Categories',
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
final currentPrefs = model.preferredCategories;
var brightness = CupertinoTheme.brightnessOf(context);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Preferred Categories'),
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: FutureBuilder<Set<VeggieCategory>>(
future: currentPrefs,
builder: (context, snapshot) {
final items = <SettingsItem>[];
for (final category in VeggieCategory.values) {
CupertinoSwitch toggle;
// It's possible that category data hasn't loaded from shared prefs
// yet, so display it if possible and fall back to disabled switches
// otherwise.
if (snapshot.hasData) {
toggle = CupertinoSwitch(
value: snapshot.data.contains(category),
onChanged: (value) {
if (value) {
model.addPreferredCategory(category);
} else {
model.removePreferredCategory(category);
}
},
);
} else {
toggle = CupertinoSwitch(
value: false,
onChanged: null,
);
}
items.add(SettingsItem(
label: veggieCategoryNames[category],
content: toggle,
));
}
return ListView(
restorationId: 'list',
children: [
SettingsGroup(
items: items,
),
],
);
},
),
),
);
}
}
class CalorieSettingsScreen extends StatelessWidget {
CalorieSettingsScreen({Key key, this.restorationId}) : super(key: key);
final String restorationId;
static const max = 1000;
static const min = 2600;
static const step = 200;
static String show(NavigatorState navigator) {
return navigator.restorablePush(_routeBuilder);
}
static Route<void> _routeBuilder(BuildContext context, Object argument) {
return CupertinoPageRoute<void>(
builder: (context) => CalorieSettingsScreen(restorationId: 'calorie'),
title: 'Calorie Target',
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
var brightness = CupertinoTheme.brightnessOf(context);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: ListView(
restorationId: 'list',
children: [
FutureBuilder<int>(
future: model.desiredCalories,
builder: (context, snapshot) {
final steps = <SettingsItem>[];
for (var cals = max; cals < min; cals += step) {
steps.add(
SettingsItem(
label: cals.toString(),
icon: SettingsIcon(
icon: Styles.checkIcon,
foregroundColor:
snapshot.hasData && snapshot.data == cals
? CupertinoColors.activeBlue
: Styles.transparentColor,
backgroundColor: Styles.transparentColor,
),
onPress: snapshot.hasData
? () => model.setDesiredCalories(cals)
: null,
),
);
}
return SettingsGroup(
items: steps,
header: SettingsGroupHeader('Available calorie levels'),
footer: SettingsGroupFooter('These are used for serving '
'calculations'),
);
},
),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
SettingsScreen({this.restorationId, Key key}) : super(key: key);
final String restorationId;
SettingsItem _buildCaloriesItem(BuildContext context, Preferences prefs) {
return SettingsItem(
label: 'Calorie Target',
icon: SettingsIcon(
backgroundColor: Styles.iconBlue,
icon: Styles.calorieIcon,
),
content: FutureBuilder<int>(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Row(
children: [
Text(
snapshot.data?.toString() ?? '',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
SizedBox(width: 8),
SettingsNavigationIndicator(),
],
);
},
),
onPress: () {
CalorieSettingsScreen.show(Navigator.of(context));
},
);
}
SettingsItem _buildCategoriesItem(BuildContext context, Preferences prefs) {
return SettingsItem(
label: 'Preferred Categories',
subtitle: 'What types of veggies you prefer!',
icon: SettingsIcon(
backgroundColor: Styles.iconGold,
icon: Styles.preferenceIcon,
),
content: SettingsNavigationIndicator(),
onPress: () {
VeggieCategorySettingsScreen.show(Navigator.of(context));
},
);
}
SettingsItem _buildRestoreDefaultsItem(
BuildContext context, Preferences prefs) {
return SettingsItem(
label: 'Restore Defaults',
icon: SettingsIcon(
backgroundColor: CupertinoColors.systemRed,
icon: Styles.resetIcon,
),
content: SettingsNavigationIndicator(),
onPress: () {
showCupertinoDialog<void>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text('Are you sure?'),
content: Text(
'Are you sure you want to reset the current settings?',
),
actions: <Widget>[
CupertinoDialogAction(
isDestructiveAction: true,
child: Text('Yes'),
onPressed: () async {
await prefs.restoreDefaults();
Navigator.pop(context);
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: Text('No'),
onPressed: () => Navigator.pop(context),
)
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final prefs = Provider.of<Preferences>(context);
return RestorationScope(
restorationId: restorationId,
child: CupertinoPageScaffold(
child: Container(
color:
Styles.scaffoldBackground(CupertinoTheme.brightnessOf(context)),
child: CustomScrollView(
restorationId: 'list',
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Settings'),
),
SliverSafeArea(
top: false,
sliver: SliverList(
delegate: SliverChildListDelegate(
<Widget>[
SettingsGroup(
items: [
_buildCaloriesItem(context, prefs),
_buildCategoriesItem(context, prefs),
_buildRestoreDefaultsItem(context, prefs),
],
),
],
),
),
),
],
),
),
),
);
}
}