Skip to content

Commit

Permalink
State Restoration support for Veggie Seasons app (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
goderbauer committed Nov 2, 2020
1 parent d30bfd5 commit ed15031
Show file tree
Hide file tree
Showing 138 changed files with 817 additions and 431 deletions.
File renamed without changes.
File renamed without changes.
42 changes: 42 additions & 0 deletions experimental/veggieseasons/README.md
Original file line number Diff line number Diff line change
@@ -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).
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ class AppState extends ChangeNotifier {
notifyListeners();
}

/// Used in tests to set the season independent of the current date.
static Season debugCurrentSeason;

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,
// but this is close enough for produce.
switch (date.month) {
Expand Down
File renamed without changes.
File renamed without changes.
89 changes: 89 additions & 0 deletions experimental/veggieseasons/lib/main.dart
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,42 @@ class InfoView extends StatelessWidget {

class DetailsScreen extends StatefulWidget {
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
_DetailsScreenState createState() => _DetailsScreenState();
}

class _DetailsScreenState extends State<DetailsScreen> {
int _selectedViewIndex = 0;
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);
Expand Down Expand Up @@ -282,33 +309,37 @@ class _DetailsScreenState extends State<DetailsScreen> {
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);

return CupertinoPageScaffold(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
children: [
_buildHeader(context, appState),
SizedBox(height: 20),
CupertinoSegmentedControl<int>(
children: {
0: Text('Facts & Info'),
1: Text('Trivia'),
},
groupValue: _selectedViewIndex,
onValueChanged: (value) {
setState(() => _selectedViewIndex = value);
},
),
_selectedViewIndex == 0
? InfoView(widget.id)
: TriviaView(widget.id),
],
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'),
],
),
),
),
],
],
),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import 'package:veggieseasons/styles.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);

Expand All @@ -32,6 +37,7 @@ class FavoritesScreen extends StatelessWidget {
),
)
: ListView(
restorationId: 'list',
children: [
SizedBox(height: 24),
for (Veggie veggie in model.favoriteVeggies)
Expand Down
55 changes: 55 additions & 0 deletions experimental/veggieseasons/lib/screens/home.dart
Original file line number Diff line number Diff line change
@@ -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');
}
},
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ 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(
Expand All @@ -29,6 +33,7 @@ class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoTabView(
restorationScopeId: restorationId,
builder: (context) {
var dateString = DateFormat('MMMM y').format(DateTime.now());

Expand All @@ -38,6 +43,7 @@ class ListScreen extends StatelessWidget {
return SafeArea(
bottom: false,
child: ListView.builder(
restorationId: 'list',
itemCount: appState.allVeggies.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
Expand Down
Loading

0 comments on commit ed15031

Please sign in to comment.