[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: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:

Loading…
Cancel
Save