[place_tracker] ChangeNotifierProvider for state management (#424)

pull/473/head
Tushar Ojha 5 years ago committed by GitHub
parent af5be70f34
commit 084c532ac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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:flutter/material.dart';
import 'package:provider/provider.dart';
import 'place_tracker_app.dart'; import 'place_tracker_app.dart';
void main() { void main() {
runApp(PlaceTrackerApp()); runApp(ChangeNotifierProvider(
create: (context) => AppState(),
child: PlaceTrackerApp(),
));
} }

@ -29,6 +29,7 @@ class Place {
final int starRating; final int starRating;
double get latitude => latLng.latitude; double get latitude => latLng.latitude;
double get longitude => latLng.longitude; double get longitude => latLng.longitude;
Place copyWith({ Place copyWith({

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'place.dart'; import 'place.dart';
import 'place_details.dart'; import 'place_details.dart';
@ -16,24 +17,27 @@ class PlaceListState extends State<PlaceList> {
void _onCategoryChanged(PlaceCategory newCategory) { void _onCategoryChanged(PlaceCategory newCategory) {
_scrollController.jumpTo(0.0); _scrollController.jumpTo(0.0);
AppState.updateWith(context, selectedCategory: newCategory); Provider.of<AppState>(context, listen: false)
.setSelectedCategory(newCategory);
} }
void _onPlaceChanged(Place value) { void _onPlaceChanged(Place value) {
// Replace the place with the modified version. // 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); final index = newPlaces.indexWhere((place) => place.id == value.id);
newPlaces[index] = value; newPlaces[index] = value;
AppState.updateWith(context, places: newPlaces); Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var state = Provider.of<AppState>(context);
return Column( return Column(
children: <Widget>[ children: <Widget>[
_ListCategoryButtonBar( _ListCategoryButtonBar(
selectedCategory: AppState.of(context).selectedCategory, selectedCategory: state.selectedCategory,
onCategoryChanged: (value) => _onCategoryChanged(value), onCategoryChanged: (value) => _onCategoryChanged(value),
), ),
Expanded( Expanded(
@ -41,10 +45,8 @@ class PlaceListState extends State<PlaceList> {
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0),
controller: _scrollController, controller: _scrollController,
shrinkWrap: true, shrinkWrap: true,
children: AppState.of(context) children: state.places
.places .where((place) => place.category == state.selectedCategory)
.where((place) =>
place.category == AppState.of(context).selectedCategory)
.map((place) => _PlaceListTile( .map((place) => _PlaceListTile(
place: place, place: place,
onPlaceChanged: (value) => _onPlaceChanged(value), onPlaceChanged: (value) => _onPlaceChanged(value),

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'place.dart'; import 'place.dart';
@ -65,7 +67,7 @@ class PlaceMapState extends State<PlaceMap> {
// Draw initial place markers on creation so that we have something // Draw initial place markers on creation so that we have something
// interesting to look at. // interesting to look at.
var markers = <Marker>{}; 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)); markers.add(await _createPlaceMarker(context, place));
} }
setState(() { setState(() {
@ -75,7 +77,7 @@ class PlaceMapState extends State<PlaceMap> {
// Zoom to fit the initially selected category. // Zoom to fit the initially selected category.
await _zoomToFitPlaces( await _zoomToFitPlaces(
_getPlacesForCategory( _getPlacesForCategory(
AppState.of(context).selectedCategory, Provider.of<AppState>(context, listen: false).selectedCategory,
_markedPlaces.values.toList(), _markedPlaces.values.toList(),
), ),
); );
@ -91,7 +93,8 @@ class PlaceMapState extends State<PlaceMap> {
onTap: () => _pushPlaceDetailsScreen(place), onTap: () => _pushPlaceDetailsScreen(place),
), ),
icon: await _getPlaceMarkerIcon(context, place.category), 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; _markedPlaces[marker] = place;
return marker; return marker;
@ -113,7 +116,8 @@ class PlaceMapState extends State<PlaceMap> {
void _onPlaceChanged(Place value) { void _onPlaceChanged(Place value) {
// Replace the place with the modified version. // 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); final index = newPlaces.indexWhere((place) => place.id == value.id);
newPlaces[index] = value; newPlaces[index] = value;
@ -124,10 +128,11 @@ class PlaceMapState extends State<PlaceMap> {
// in the main build method due to a modified AppState. // in the main build method due to a modified AppState.
_configuration = MapConfiguration( _configuration = MapConfiguration(
places: newPlaces, 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}) { void _updateExistingPlaceMarker({@required Place place}) {
@ -159,7 +164,7 @@ class PlaceMapState extends State<PlaceMap> {
} }
Future<void> _switchSelectedCategory(PlaceCategory category) async { Future<void> _switchSelectedCategory(PlaceCategory category) async {
AppState.updateWith(context, selectedCategory: category); Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
await _showPlacesForSelectedCategory(category); await _showPlacesForSelectedCategory(category);
} }
@ -233,11 +238,12 @@ class PlaceMapState extends State<PlaceMap> {
id: Uuid().v1(), id: Uuid().v1(),
latLng: _pendingMarker.position, latLng: _pendingMarker.position,
name: _pendingMarker.infoWindow.title, name: _pendingMarker.infoWindow.title,
category: AppState.of(context).selectedCategory, category:
Provider.of<AppState>(context, listen: false).selectedCategory,
); );
var placeMarker = await _getPlaceMarkerIcon( var placeMarker = await _getPlaceMarkerIcon(context,
context, AppState.of(context).selectedCategory); Provider.of<AppState>(context, listen: false).selectedCategory);
setState(() { setState(() {
final updatedMarker = _pendingMarker.copyWith( final updatedMarker = _pendingMarker.copyWith(
@ -275,18 +281,20 @@ class PlaceMapState extends State<PlaceMap> {
); );
// Add the new place to the places stored in appState. // Add the new place to the places stored in appState.
final newPlaces = List<Place>.from(AppState.of(context).places) final newPlaces =
..add(newPlace); List<Place>.from(Provider.of<AppState>(context, listen: false).places)
..add(newPlace);
// Manually update our map configuration here since our map is already // Manually update our map configuration here since our map is already
// updated with the new marker. Otherwise, the map would be reconfigured // updated with the new marker. Otherwise, the map would be reconfigured
// in the main build method due to a modified AppState. // in the main build method due to a modified AppState.
_configuration = MapConfiguration( _configuration = MapConfiguration(
places: newPlaces, 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 { Future<void> _maybeUpdateMapConfiguration() async {
_configuration ??= MapConfiguration.of(AppState.of(context)); _configuration ??=
final newConfiguration = MapConfiguration.of(AppState.of(context)); 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 // Since we manually update [_configuration] when place or selectedCategory
// changes come from the [place_map], we should only enter this if statement // changes come from the [place_map], we should only enter this if statement
@ -344,6 +354,7 @@ class PlaceMapState extends State<PlaceMap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_maybeUpdateMapConfiguration(); _maybeUpdateMapConfiguration();
var state = Provider.of<AppState>(context);
return Builder(builder: (context) { return Builder(builder: (context) {
// We need this additional builder here so that we can pass its context to // 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, onCameraMove: (position) => _lastMapPosition = position.target,
), ),
_CategoryButtonBar( _CategoryButtonBar(
selectedPlaceCategory: AppState.of(context).selectedCategory, selectedPlaceCategory: state.selectedCategory,
visible: _pendingMarker == null, visible: _pendingMarker == null,
onChanged: _switchSelectedCategory, onChanged: _switchSelectedCategory,
), ),

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:provider/provider.dart';
import 'app_model.dart';
import 'place.dart'; import 'place.dart';
import 'place_list.dart'; import 'place_list.dart';
import 'place_map.dart'; import 'place_map.dart';
@ -12,23 +12,10 @@ enum PlaceTrackerViewType {
list, list,
} }
class PlaceTrackerApp extends StatefulWidget { class PlaceTrackerApp extends StatelessWidget {
@override
_PlaceTrackerAppState createState() => _PlaceTrackerAppState();
}
class _PlaceTrackerAppState extends State<PlaceTrackerApp> {
AppState appState = AppState();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
builder: (context, child) {
return AppModel<AppState>(
initialState: AppState(),
child: child,
);
},
home: _PlaceTrackerHomePage(), home: _PlaceTrackerHomePage(),
); );
} }
@ -39,6 +26,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var state = Provider.of<AppState>(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
@ -57,18 +45,16 @@ class _PlaceTrackerHomePage extends StatelessWidget {
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0), padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
child: IconButton( child: IconButton(
icon: Icon( icon: Icon(
AppState.of(context).viewType == PlaceTrackerViewType.map state.viewType == PlaceTrackerViewType.map
? Icons.list ? Icons.list
: Icons.map, : Icons.map,
size: 32.0, size: 32.0,
), ),
onPressed: () { onPressed: () {
AppState.updateWith( state.setViewType(
context, state.viewType == PlaceTrackerViewType.map
viewType: ? PlaceTrackerViewType.list
AppState.of(context).viewType == PlaceTrackerViewType.map : PlaceTrackerViewType.map,
? PlaceTrackerViewType.list
: PlaceTrackerViewType.map,
); );
}, },
), ),
@ -76,61 +62,41 @@ class _PlaceTrackerHomePage extends StatelessWidget {
], ],
), ),
body: IndexedStack( body: IndexedStack(
index: index: state.viewType == PlaceTrackerViewType.map ? 0 : 1,
AppState.of(context).viewType == PlaceTrackerViewType.map ? 0 : 1,
children: <Widget>[ children: <Widget>[
PlaceMap(center: const LatLng(45.521563, -122.677433)), PlaceMap(center: const LatLng(45.521563, -122.677433)),
PlaceList(), PlaceList()
], ],
), ),
); );
} }
} }
class AppState { class AppState extends ChangeNotifier {
const AppState({ AppState({
this.places = StubData.places, this.places = StubData.places,
this.selectedCategory = PlaceCategory.favorite, this.selectedCategory = PlaceCategory.favorite,
this.viewType = PlaceTrackerViewType.map, this.viewType = PlaceTrackerViewType.map,
}) : assert(places != null), }) : assert(places != null),
assert(selectedCategory != null); assert(selectedCategory != null);
final List<Place> places; List<Place> places;
final PlaceCategory selectedCategory; PlaceCategory selectedCategory;
final PlaceTrackerViewType viewType; PlaceTrackerViewType viewType;
AppState copyWith({ void setViewType(PlaceTrackerViewType viewType) {
List<Place> places, this.viewType = viewType;
PlaceCategory selectedCategory, notifyListeners();
PlaceTrackerViewType viewType,
}) {
return AppState(
places: places ?? this.places,
selectedCategory: selectedCategory ?? this.selectedCategory,
viewType: viewType ?? this.viewType,
);
} }
static AppState of(BuildContext context) => AppModel.of<AppState>(context); void setSelectedCategory(PlaceCategory newCategory) {
selectedCategory = newCategory;
static void update(BuildContext context, AppState newState) { notifyListeners();
AppModel.update<AppState>(context, newState);
} }
static void updateWith( void setPlaces(List<Place> newPlaces) {
BuildContext context, { places = newPlaces;
List<Place> places, notifyListeners();
PlaceCategory selectedCategory,
PlaceTrackerViewType viewType,
}) {
update(
context,
AppState.of(context).copyWith(
places: places,
selectedCategory: selectedCategory,
viewType: viewType,
),
);
} }
@override @override

@ -116,6 +116,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.8" version: "1.1.8"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -144,6 +151,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
provider:
dependency: "direct dev"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.5+1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:

@ -18,6 +18,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
pedantic: ^1.9.0 pedantic: ^1.9.0
provider: ^4.0.5+1
flutter: flutter:
assets: assets:

Loading…
Cancel
Save