State Restoration support for Veggie Seasons app (#433)
42
experimental/veggieseasons/README.md
Normal file
@@ -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 |
@@ -38,7 +38,14 @@ class AppState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used in tests to set the season independent of the current date.
|
||||||
|
static Season debugCurrentSeason;
|
||||||
|
|
||||||
static Season _getSeasonForDate(DateTime date) {
|
static Season _getSeasonForDate(DateTime date) {
|
||||||
|
if (debugCurrentSeason != null) {
|
||||||
|
return debugCurrentSeason;
|
||||||
|
}
|
||||||
|
|
||||||
// Technically the start and end dates of seasons can vary by a day or so,
|
// Technically the start and end dates of seasons can vary by a day or so,
|
||||||
// but this is close enough for produce.
|
// but this is close enough for produce.
|
||||||
switch (date.month) {
|
switch (date.month) {
|
||||||
89
experimental/veggieseasons/lib/main.dart
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,15 +238,42 @@ class InfoView extends StatelessWidget {
|
|||||||
|
|
||||||
class DetailsScreen extends StatefulWidget {
|
class DetailsScreen extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
|
final String restorationId;
|
||||||
|
|
||||||
DetailsScreen(this.id);
|
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
|
@override
|
||||||
_DetailsScreenState createState() => _DetailsScreenState();
|
_DetailsScreenState createState() => _DetailsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailsScreenState extends State<DetailsScreen> {
|
class _DetailsScreenState extends State<DetailsScreen> with RestorationMixin {
|
||||||
int _selectedViewIndex = 0;
|
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) {
|
Widget _buildHeader(BuildContext context, AppState model) {
|
||||||
final veggie = model.getVeggie(widget.id);
|
final veggie = model.getVeggie(widget.id);
|
||||||
@@ -282,13 +309,16 @@ class _DetailsScreenState extends State<DetailsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appState = Provider.of<AppState>(context);
|
final appState = Provider.of<AppState>(context);
|
||||||
|
|
||||||
return CupertinoPageScaffold(
|
return UnmanagedRestorationScope(
|
||||||
|
bucket: bucket,
|
||||||
|
child: CupertinoPageScaffold(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
restorationId: 'list',
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(context, appState),
|
_buildHeader(context, appState),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
@@ -297,19 +327,20 @@ class _DetailsScreenState extends State<DetailsScreen> {
|
|||||||
0: Text('Facts & Info'),
|
0: Text('Facts & Info'),
|
||||||
1: Text('Trivia'),
|
1: Text('Trivia'),
|
||||||
},
|
},
|
||||||
groupValue: _selectedViewIndex,
|
groupValue: _selectedViewIndex.value,
|
||||||
onValueChanged: (value) {
|
onValueChanged: (value) {
|
||||||
setState(() => _selectedViewIndex = value);
|
setState(() => _selectedViewIndex.value = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_selectedViewIndex == 0
|
_selectedViewIndex.value == 0
|
||||||
? InfoView(widget.id)
|
? InfoView(widget.id)
|
||||||
: TriviaView(widget.id),
|
: TriviaView(id: widget.id, restorationId: 'trivia'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,14 @@ import 'package:veggieseasons/styles.dart';
|
|||||||
import 'package:veggieseasons/widgets/veggie_headline.dart';
|
import 'package:veggieseasons/widgets/veggie_headline.dart';
|
||||||
|
|
||||||
class FavoritesScreen extends StatelessWidget {
|
class FavoritesScreen extends StatelessWidget {
|
||||||
|
FavoritesScreen({this.restorationId, Key key}) : super(key: key);
|
||||||
|
|
||||||
|
final String restorationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CupertinoTabView(
|
return CupertinoTabView(
|
||||||
|
restorationScopeId: restorationId,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final model = Provider.of<AppState>(context);
|
final model = Provider.of<AppState>(context);
|
||||||
|
|
||||||
@@ -32,6 +37,7 @@ class FavoritesScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ListView(
|
: ListView(
|
||||||
|
restorationId: 'list',
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
for (Veggie veggie in model.favoriteVeggies)
|
for (Veggie veggie in model.favoriteVeggies)
|
||||||
55
experimental/veggieseasons/lib/screens/home.dart
Normal 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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ import 'package:veggieseasons/styles.dart';
|
|||||||
import 'package:veggieseasons/widgets/veggie_card.dart';
|
import 'package:veggieseasons/widgets/veggie_card.dart';
|
||||||
|
|
||||||
class ListScreen extends StatelessWidget {
|
class ListScreen extends StatelessWidget {
|
||||||
|
ListScreen({this.restorationId, Key key}) : super(key: key);
|
||||||
|
|
||||||
|
final String restorationId;
|
||||||
|
|
||||||
Widget _generateVeggieRow(Veggie veggie, Preferences prefs,
|
Widget _generateVeggieRow(Veggie veggie, Preferences prefs,
|
||||||
{bool inSeason = true}) {
|
{bool inSeason = true}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -29,6 +33,7 @@ class ListScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CupertinoTabView(
|
return CupertinoTabView(
|
||||||
|
restorationScopeId: restorationId,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
var dateString = DateFormat('MMMM y').format(DateTime.now());
|
var dateString = DateFormat('MMMM y').format(DateTime.now());
|
||||||
|
|
||||||
@@ -38,6 +43,7 @@ class ListScreen extends StatelessWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
restorationId: 'list',
|
||||||
itemCount: appState.allVeggies.length + 2,
|
itemCount: appState.allVeggies.length + 2,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
@@ -12,36 +12,45 @@ import 'package:veggieseasons/widgets/search_bar.dart';
|
|||||||
import 'package:veggieseasons/widgets/veggie_headline.dart';
|
import 'package:veggieseasons/widgets/veggie_headline.dart';
|
||||||
|
|
||||||
class SearchScreen extends StatefulWidget {
|
class SearchScreen extends StatefulWidget {
|
||||||
|
SearchScreen({this.restorationId, Key key}) : super(key: key);
|
||||||
|
|
||||||
|
final String restorationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SearchScreenState createState() => _SearchScreenState();
|
_SearchScreenState createState() => _SearchScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchScreenState extends State<SearchScreen> {
|
class _SearchScreenState extends State<SearchScreen> with RestorationMixin {
|
||||||
final controller = TextEditingController();
|
final controller = RestorableTextEditingController();
|
||||||
final focusNode = FocusNode();
|
final focusNode = FocusNode();
|
||||||
String terms = '';
|
String terms;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
String get restorationId => widget.restorationId;
|
||||||
super.initState();
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(controller, 'text');
|
||||||
controller.addListener(_onTextChanged);
|
controller.addListener(_onTextChanged);
|
||||||
|
terms = controller.value.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
focusNode.dispose();
|
focusNode.dispose();
|
||||||
|
controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTextChanged() {
|
void _onTextChanged() {
|
||||||
setState(() => terms = controller.text);
|
setState(() => terms = controller.value.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _createSearchBox() {
|
Widget _createSearchBox() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: SearchBar(
|
child: SearchBar(
|
||||||
controller: controller,
|
controller: controller.value,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -61,6 +70,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
restorationId: 'list',
|
||||||
itemCount: veggies.length + 1,
|
itemCount: veggies.length + 1,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
@@ -88,7 +98,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final model = Provider.of<AppState>(context);
|
final model = Provider.of<AppState>(context);
|
||||||
|
|
||||||
return CupertinoTabView(
|
return UnmanagedRestorationScope(
|
||||||
|
child: CupertinoTabView(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
@@ -100,6 +111,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
250
experimental/veggieseasons/lib/screens/settings.dart
Normal file
@@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,9 @@ import 'package:veggieseasons/styles.dart';
|
|||||||
/// the user's score.
|
/// the user's score.
|
||||||
class TriviaView extends StatefulWidget {
|
class TriviaView extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
|
final String restorationId;
|
||||||
|
|
||||||
const TriviaView(this.id);
|
const TriviaView({this.id, this.restorationId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_TriviaViewState createState() => _TriviaViewState();
|
_TriviaViewState createState() => _TriviaViewState();
|
||||||
@@ -23,7 +24,7 @@ enum PlayerStatus {
|
|||||||
wasIncorrect,
|
wasIncorrect,
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TriviaViewState extends State<TriviaView> {
|
class _TriviaViewState extends State<TriviaView> with RestorationMixin {
|
||||||
/// Current app state. This is used to fetch veggie data.
|
/// Current app state. This is used to fetch veggie data.
|
||||||
AppState appState;
|
AppState appState;
|
||||||
|
|
||||||
@@ -31,16 +32,26 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
Veggie veggie;
|
Veggie veggie;
|
||||||
|
|
||||||
/// Index of the current trivia question.
|
/// Index of the current trivia question.
|
||||||
int triviaIndex = 0;
|
RestorableInt triviaIndex = RestorableInt(0);
|
||||||
|
|
||||||
/// User's score on the current veggie.
|
/// User's score on the current veggie.
|
||||||
int score = 0;
|
RestorableInt score = RestorableInt(0);
|
||||||
|
|
||||||
/// Trivia question currently being displayed.
|
/// Trivia question currently being displayed.
|
||||||
Trivia get currentTrivia => veggie.trivia[triviaIndex];
|
Trivia get currentTrivia => veggie.trivia[triviaIndex.value];
|
||||||
|
|
||||||
/// The current state of the game.
|
/// The current state of the game.
|
||||||
PlayerStatus status = PlayerStatus.readyToAnswer;
|
_RestorablePlayerStatus status = _RestorablePlayerStatus(PlayerStatus.readyToAnswer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get restorationId => widget.restorationId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(triviaIndex, 'index');
|
||||||
|
registerForRestoration(score, 'score');
|
||||||
|
registerForRestoration(status, 'status');
|
||||||
|
}
|
||||||
|
|
||||||
// Called at init and again if any dependencies (read: InheritedWidgets) on
|
// Called at init and again if any dependencies (read: InheritedWidgets) on
|
||||||
// on which this object relies are changed.
|
// on which this object relies are changed.
|
||||||
@@ -72,11 +83,19 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
triviaIndex.dispose();
|
||||||
|
score.dispose();
|
||||||
|
status.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (triviaIndex >= veggie.trivia.length) {
|
if (triviaIndex.value >= veggie.trivia.length) {
|
||||||
return _buildFinishedView();
|
return _buildFinishedView();
|
||||||
} else if (status == PlayerStatus.readyToAnswer) {
|
} else if (status.value == PlayerStatus.readyToAnswer) {
|
||||||
return _buildQuestionView();
|
return _buildQuestionView();
|
||||||
} else {
|
} else {
|
||||||
return _buildResultView();
|
return _buildResultView();
|
||||||
@@ -85,19 +104,19 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
|
|
||||||
void _resetGame() {
|
void _resetGame() {
|
||||||
setState(() {
|
setState(() {
|
||||||
triviaIndex = 0;
|
triviaIndex.value = 0;
|
||||||
score = 0;
|
score.value = 0;
|
||||||
status = PlayerStatus.readyToAnswer;
|
status.value = PlayerStatus.readyToAnswer;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _processAnswer(int answerIndex) {
|
void _processAnswer(int answerIndex) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (answerIndex == currentTrivia.correctAnswerIndex) {
|
if (answerIndex == currentTrivia.correctAnswerIndex) {
|
||||||
status = PlayerStatus.wasCorrect;
|
status.value = PlayerStatus.wasCorrect;
|
||||||
score++;
|
score.value++;
|
||||||
} else {
|
} else {
|
||||||
status = PlayerStatus.wasIncorrect;
|
status.value = PlayerStatus.wasIncorrect;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,7 +212,7 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
status == PlayerStatus.wasCorrect
|
status.value == PlayerStatus.wasCorrect
|
||||||
? 'That\'s right!'
|
? 'That\'s right!'
|
||||||
: 'Sorry, that wasn\'t the right answer.',
|
: 'Sorry, that wasn\'t the right answer.',
|
||||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||||
@@ -202,8 +221,8 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
child: Text('Next Question'),
|
child: Text('Next Question'),
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
triviaIndex++;
|
triviaIndex.value++;
|
||||||
status = PlayerStatus.readyToAnswer;
|
status.value = PlayerStatus.readyToAnswer;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -211,3 +230,29 @@ class _TriviaViewState extends State<TriviaView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RestorablePlayerStatus extends RestorableValue<PlayerStatus> {
|
||||||
|
_RestorablePlayerStatus(this._defaultValue);
|
||||||
|
|
||||||
|
final PlayerStatus _defaultValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PlayerStatus createDefaultValue() {
|
||||||
|
return _defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PlayerStatus fromPrimitives(Object data) {
|
||||||
|
return PlayerStatus.values[data as int];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object toPrimitives() {
|
||||||
|
return value.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateValue(PlayerStatus oldValue) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,12 +142,7 @@ class VeggieCard extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PressableCard(
|
return PressableCard(
|
||||||
onPressed: () {
|
onPressed: () => DetailsScreen.show(Navigator.of(context), veggie.id),
|
||||||
Navigator.of(context).push<void>(CupertinoPageRoute(
|
|
||||||
builder: (context) => DetailsScreen(veggie.id),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Semantics(
|
Semantics(
|
||||||
@@ -70,10 +70,7 @@ class VeggieHeadline extends StatelessWidget {
|
|||||||
final themeData = CupertinoTheme.of(context);
|
final themeData = CupertinoTheme.of(context);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.of(context).push<void>(CupertinoPageRoute(
|
onTap: () => DetailsScreen.show(Navigator.of(context), veggie.id),
|
||||||
builder: (context) => DetailsScreen(veggie.id),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
)),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||