State Restoration support for Veggie Seasons app (#433)
@ -0,0 +1,42 @@
|
||||
# Veggie Seasons
|
||||
|
||||
An iOS app that shows which fruits and vegetables are currently in season. It
|
||||
showcases Flutter's Cupertino package.
|
||||
|
||||
[Available now in the App Store!](https://itunes.apple.com/is/app/veggie-seasons/id1450855435)
|
||||
|
||||
**NOTE:** While Flutter supports many platforms, this application is designed
|
||||
specifically for iOS. It's not intended to be run on Android, web, or desktop.
|
||||
|
||||
## Goals
|
||||
|
||||
* Show how to build an interface that iOS users will feel right at home
|
||||
with.
|
||||
* Show how Flutter's Cupertino widgets work together.
|
||||
|
||||
## The important bits
|
||||
|
||||
### `/screens/*`
|
||||
|
||||
These are the screens presented in the app, roughly analogous to
|
||||
UIViewControllers. `HomeScreen` is the root, and the others are shown
|
||||
as the user navigates.
|
||||
|
||||
### `/widgets/search_bar.dart`
|
||||
|
||||
An example of how to construct an Cupertino-style search bar. The
|
||||
Flutter team [is working on an official widget](https://github.com/flutter/flutter/issues/9784)
|
||||
for this. Once that effort is complete, developers will not need to roll
|
||||
their own search bars, so to speak.
|
||||
|
||||
## Questions/issues
|
||||
|
||||
If you have a general question about any of the techniques you see in
|
||||
the sample, the best places to go are:
|
||||
|
||||
* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev)
|
||||
* [The Flutter Gitter channel](https://gitter.im/flutter/flutter)
|
||||
* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter)
|
||||
|
||||
If you run into an issue with the sample itself, please file an issue
|
||||
in the [main Flutter repo](https://github.com/flutter/flutter/issues).
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 281 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 271 KiB |
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 274 KiB |
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 685 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 1012 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
@ -0,0 +1,89 @@
|
||||
// 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' show DeviceOrientation, SystemChrome;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:veggieseasons/data/app_state.dart';
|
||||
import 'package:veggieseasons/data/preferences.dart';
|
||||
import 'package:veggieseasons/screens/home.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
runApp(
|
||||
RootRestorationScope(
|
||||
restorationId: 'root',
|
||||
child: VeggieApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class VeggieApp extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VeggieAppState();
|
||||
}
|
||||
|
||||
class _VeggieAppState extends State<VeggieApp> with RestorationMixin {
|
||||
final _RestorableAppState _appState = _RestorableAppState();
|
||||
|
||||
@override
|
||||
String get restorationId => 'wrapper';
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
registerForRestoration(_appState, 'state');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(
|
||||
value: _appState.value,
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => Preferences()..load(),
|
||||
),
|
||||
],
|
||||
child: CupertinoApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: HomeScreen(restorationId: 'home'),
|
||||
restorationScopeId: 'app',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RestorableAppState extends RestorableListenable<AppState> {
|
||||
@override
|
||||
AppState createDefaultValue() {
|
||||
return AppState();
|
||||
}
|
||||
|
||||
@override
|
||||
AppState fromPrimitives(Object data) {
|
||||
final appState = AppState();
|
||||
final favorites = (data as List<dynamic>).cast<int>();
|
||||
for (var id in favorites) {
|
||||
appState.setFavorite(id, true);
|
||||
}
|
||||
return appState;
|
||||
}
|
||||
|
||||
@override
|
||||
Object toPrimitives() {
|
||||
return value.favoriteVeggies.map((veggie) => veggie.id).toList();
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
// 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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|