mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
[place_tracker] ChangeNotifierProvider for state management (#424)
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class _AppModelScope<T> extends InheritedWidget {
|
||||
const _AppModelScope({
|
||||
Key key,
|
||||
this.appModelState,
|
||||
Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final _AppModelState<T> appModelState;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_AppModelScope oldWidget) => true;
|
||||
}
|
||||
|
||||
class AppModel<T> extends StatefulWidget {
|
||||
AppModel({
|
||||
Key key,
|
||||
@required this.initialState,
|
||||
this.child,
|
||||
}) : assert(initialState != null),
|
||||
super(key: key);
|
||||
|
||||
final T initialState;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_AppModelState<T> createState() => _AppModelState<T>();
|
||||
|
||||
static T of<T>(BuildContext context) {
|
||||
final scope =
|
||||
context.dependOnInheritedWidgetOfExactType<_AppModelScope<T>>();
|
||||
return scope.appModelState.currentState;
|
||||
}
|
||||
|
||||
static void update<T>(BuildContext context, T newState) {
|
||||
final scope =
|
||||
context.dependOnInheritedWidgetOfExactType<_AppModelScope<T>>();
|
||||
scope.appModelState.updateState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppModelState<T> extends State<AppModel<T>> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentState = widget.initialState;
|
||||
}
|
||||
|
||||
T currentState;
|
||||
|
||||
void updateState(T newState) {
|
||||
if (newState != currentState) {
|
||||
setState(() {
|
||||
currentState = newState;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _AppModelScope<T>(
|
||||
appModelState: this,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'place_tracker_app.dart';
|
||||
|
||||
void main() {
|
||||
runApp(PlaceTrackerApp());
|
||||
runApp(ChangeNotifierProvider(
|
||||
create: (context) => AppState(),
|
||||
child: PlaceTrackerApp(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class Place {
|
||||
final int starRating;
|
||||
|
||||
double get latitude => latLng.latitude;
|
||||
|
||||
double get longitude => latLng.longitude;
|
||||
|
||||
Place copyWith({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'place.dart';
|
||||
import 'place_details.dart';
|
||||
@@ -16,24 +17,27 @@ class PlaceListState extends State<PlaceList> {
|
||||
|
||||
void _onCategoryChanged(PlaceCategory newCategory) {
|
||||
_scrollController.jumpTo(0.0);
|
||||
AppState.updateWith(context, selectedCategory: newCategory);
|
||||
Provider.of<AppState>(context, listen: false)
|
||||
.setSelectedCategory(newCategory);
|
||||
}
|
||||
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces = List<Place>.from(AppState.of(context).places);
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
AppState.updateWith(context, places: newPlaces);
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var state = Provider.of<AppState>(context);
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
_ListCategoryButtonBar(
|
||||
selectedCategory: AppState.of(context).selectedCategory,
|
||||
selectedCategory: state.selectedCategory,
|
||||
onCategoryChanged: (value) => _onCategoryChanged(value),
|
||||
),
|
||||
Expanded(
|
||||
@@ -41,10 +45,8 @@ class PlaceListState extends State<PlaceList> {
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0),
|
||||
controller: _scrollController,
|
||||
shrinkWrap: true,
|
||||
children: AppState.of(context)
|
||||
.places
|
||||
.where((place) =>
|
||||
place.category == AppState.of(context).selectedCategory)
|
||||
children: state.places
|
||||
.where((place) => place.category == state.selectedCategory)
|
||||
.map((place) => _PlaceListTile(
|
||||
place: place,
|
||||
onPlaceChanged: (value) => _onPlaceChanged(value),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'place.dart';
|
||||
@@ -65,7 +67,7 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
// Draw initial place markers on creation so that we have something
|
||||
// interesting to look at.
|
||||
var markers = <Marker>{};
|
||||
for (var place in AppState.of(context).places) {
|
||||
for (var place in Provider.of<AppState>(context, listen: false).places) {
|
||||
markers.add(await _createPlaceMarker(context, place));
|
||||
}
|
||||
setState(() {
|
||||
@@ -75,7 +77,7 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
// Zoom to fit the initially selected category.
|
||||
await _zoomToFitPlaces(
|
||||
_getPlacesForCategory(
|
||||
AppState.of(context).selectedCategory,
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
_markedPlaces.values.toList(),
|
||||
),
|
||||
);
|
||||
@@ -91,7 +93,8 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
onTap: () => _pushPlaceDetailsScreen(place),
|
||||
),
|
||||
icon: await _getPlaceMarkerIcon(context, place.category),
|
||||
visible: place.category == AppState.of(context).selectedCategory,
|
||||
visible: place.category ==
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
_markedPlaces[marker] = place;
|
||||
return marker;
|
||||
@@ -113,7 +116,8 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces = List<Place>.from(AppState.of(context).places);
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
@@ -124,10 +128,11 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
// in the main build method due to a modified AppState.
|
||||
_configuration = MapConfiguration(
|
||||
places: newPlaces,
|
||||
selectedCategory: AppState.of(context).selectedCategory,
|
||||
selectedCategory:
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
|
||||
AppState.updateWith(context, places: newPlaces);
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
|
||||
void _updateExistingPlaceMarker({@required Place place}) {
|
||||
@@ -159,7 +164,7 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
}
|
||||
|
||||
Future<void> _switchSelectedCategory(PlaceCategory category) async {
|
||||
AppState.updateWith(context, selectedCategory: category);
|
||||
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
|
||||
await _showPlacesForSelectedCategory(category);
|
||||
}
|
||||
|
||||
@@ -233,11 +238,12 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
id: Uuid().v1(),
|
||||
latLng: _pendingMarker.position,
|
||||
name: _pendingMarker.infoWindow.title,
|
||||
category: AppState.of(context).selectedCategory,
|
||||
category:
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
|
||||
var placeMarker = await _getPlaceMarkerIcon(
|
||||
context, AppState.of(context).selectedCategory);
|
||||
var placeMarker = await _getPlaceMarkerIcon(context,
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory);
|
||||
|
||||
setState(() {
|
||||
final updatedMarker = _pendingMarker.copyWith(
|
||||
@@ -275,18 +281,20 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
);
|
||||
|
||||
// Add the new place to the places stored in appState.
|
||||
final newPlaces = List<Place>.from(AppState.of(context).places)
|
||||
..add(newPlace);
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places)
|
||||
..add(newPlace);
|
||||
|
||||
// Manually update our map configuration here since our map is already
|
||||
// updated with the new marker. Otherwise, the map would be reconfigured
|
||||
// in the main build method due to a modified AppState.
|
||||
_configuration = MapConfiguration(
|
||||
places: newPlaces,
|
||||
selectedCategory: AppState.of(context).selectedCategory,
|
||||
selectedCategory:
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
|
||||
AppState.updateWith(context, places: newPlaces);
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,8 +317,10 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
}
|
||||
|
||||
Future<void> _maybeUpdateMapConfiguration() async {
|
||||
_configuration ??= MapConfiguration.of(AppState.of(context));
|
||||
final newConfiguration = MapConfiguration.of(AppState.of(context));
|
||||
_configuration ??=
|
||||
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
|
||||
final newConfiguration =
|
||||
MapConfiguration.of(Provider.of<AppState>(context, listen: false));
|
||||
|
||||
// Since we manually update [_configuration] when place or selectedCategory
|
||||
// changes come from the [place_map], we should only enter this if statement
|
||||
@@ -344,6 +354,7 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_maybeUpdateMapConfiguration();
|
||||
var state = Provider.of<AppState>(context);
|
||||
|
||||
return Builder(builder: (context) {
|
||||
// We need this additional builder here so that we can pass its context to
|
||||
@@ -364,7 +375,7 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
onCameraMove: (position) => _lastMapPosition = position.target,
|
||||
),
|
||||
_CategoryButtonBar(
|
||||
selectedPlaceCategory: AppState.of(context).selectedCategory,
|
||||
selectedPlaceCategory: state.selectedCategory,
|
||||
visible: _pendingMarker == null,
|
||||
onChanged: _switchSelectedCategory,
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'app_model.dart';
|
||||
import 'place.dart';
|
||||
import 'place_list.dart';
|
||||
import 'place_map.dart';
|
||||
@@ -12,23 +12,10 @@ enum PlaceTrackerViewType {
|
||||
list,
|
||||
}
|
||||
|
||||
class PlaceTrackerApp extends StatefulWidget {
|
||||
@override
|
||||
_PlaceTrackerAppState createState() => _PlaceTrackerAppState();
|
||||
}
|
||||
|
||||
class _PlaceTrackerAppState extends State<PlaceTrackerApp> {
|
||||
AppState appState = AppState();
|
||||
|
||||
class PlaceTrackerApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) {
|
||||
return AppModel<AppState>(
|
||||
initialState: AppState(),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
home: _PlaceTrackerHomePage(),
|
||||
);
|
||||
}
|
||||
@@ -39,6 +26,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var state = Provider.of<AppState>(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
@@ -57,18 +45,16 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
AppState.of(context).viewType == PlaceTrackerViewType.map
|
||||
state.viewType == PlaceTrackerViewType.map
|
||||
? Icons.list
|
||||
: Icons.map,
|
||||
size: 32.0,
|
||||
),
|
||||
onPressed: () {
|
||||
AppState.updateWith(
|
||||
context,
|
||||
viewType:
|
||||
AppState.of(context).viewType == PlaceTrackerViewType.map
|
||||
? PlaceTrackerViewType.list
|
||||
: PlaceTrackerViewType.map,
|
||||
state.setViewType(
|
||||
state.viewType == PlaceTrackerViewType.map
|
||||
? PlaceTrackerViewType.list
|
||||
: PlaceTrackerViewType.map,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -76,61 +62,41 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
body: IndexedStack(
|
||||
index:
|
||||
AppState.of(context).viewType == PlaceTrackerViewType.map ? 0 : 1,
|
||||
index: state.viewType == PlaceTrackerViewType.map ? 0 : 1,
|
||||
children: <Widget>[
|
||||
PlaceMap(center: const LatLng(45.521563, -122.677433)),
|
||||
PlaceList(),
|
||||
PlaceList()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppState {
|
||||
const AppState({
|
||||
class AppState extends ChangeNotifier {
|
||||
AppState({
|
||||
this.places = StubData.places,
|
||||
this.selectedCategory = PlaceCategory.favorite,
|
||||
this.viewType = PlaceTrackerViewType.map,
|
||||
}) : assert(places != null),
|
||||
assert(selectedCategory != null);
|
||||
|
||||
final List<Place> places;
|
||||
final PlaceCategory selectedCategory;
|
||||
final PlaceTrackerViewType viewType;
|
||||
List<Place> places;
|
||||
PlaceCategory selectedCategory;
|
||||
PlaceTrackerViewType viewType;
|
||||
|
||||
AppState copyWith({
|
||||
List<Place> places,
|
||||
PlaceCategory selectedCategory,
|
||||
PlaceTrackerViewType viewType,
|
||||
}) {
|
||||
return AppState(
|
||||
places: places ?? this.places,
|
||||
selectedCategory: selectedCategory ?? this.selectedCategory,
|
||||
viewType: viewType ?? this.viewType,
|
||||
);
|
||||
void setViewType(PlaceTrackerViewType viewType) {
|
||||
this.viewType = viewType;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static AppState of(BuildContext context) => AppModel.of<AppState>(context);
|
||||
|
||||
static void update(BuildContext context, AppState newState) {
|
||||
AppModel.update<AppState>(context, newState);
|
||||
void setSelectedCategory(PlaceCategory newCategory) {
|
||||
selectedCategory = newCategory;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static void updateWith(
|
||||
BuildContext context, {
|
||||
List<Place> places,
|
||||
PlaceCategory selectedCategory,
|
||||
PlaceTrackerViewType viewType,
|
||||
}) {
|
||||
update(
|
||||
context,
|
||||
AppState.of(context).copyWith(
|
||||
places: places,
|
||||
selectedCategory: selectedCategory,
|
||||
viewType: viewType,
|
||||
),
|
||||
);
|
||||
void setPlaces(List<Place> newPlaces) {
|
||||
places = newPlaces;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -116,6 +116,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -144,6 +151,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
provider:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.5+1"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -18,6 +18,7 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
pedantic: ^1.9.0
|
||||
provider: ^4.0.5+1
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
|
||||
Reference in New Issue
Block a user