Skip to content

Commit

Permalink
fix(client): refetch overrides fetchPolicy
Browse files Browse the repository at this point in the history
  • Loading branch information
micimize committed Oct 16, 2020
1 parent d810113 commit 891bc2b
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 103 deletions.
74 changes: 14 additions & 60 deletions examples/starwars/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,75 +10,29 @@ project 'Runner', {
'Release' => :release,
}

def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end

target 'Runner' do
# Flutter Pod

copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.

generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];

unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

# Plugin Pods
flutter_ios_podfile_setup

# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
flutter_additional_ios_build_settings(target)
end
end
13 changes: 13 additions & 0 deletions examples/starwars/lib/episode/episode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ String episodeToJson(Episode e) {
}
}

String episodeToDisplay(Episode e) {
switch (e) {
case Episode.NEWHOPE:
return 'EP. IV: A NEW HOPE';
case Episode.EMPIRE:
return 'EP. V: THE EMPIRE STRIKES BACK';
case Episode.JEDI:
return 'EP. VI: RETURN OF THE JEDI';
default:
return null;
}
}

Episode episodeFromJson(String e) {
switch (e) {
case 'NEWHOPE':
Expand Down
2 changes: 1 addition & 1 deletion examples/starwars/lib/episode/episode_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import './hero_query.dart';
class EpisodePage extends StatefulWidget {
static const BottomNavigationBarItem navItem = BottomNavigationBarItem(
icon: Icon(Icons.movie_filter),
title: Text('Episodes'),
label: 'Episodes',
);

@override
Expand Down
24 changes: 16 additions & 8 deletions examples/starwars/lib/episode/hero_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,29 @@ class HeroForEpisode extends StatelessWidget {
Future<QueryResult> Function() refetch,
FetchMore fetchMore,
}) {
// NOTE: a loading message is always sent, but if you're developing locally,
// the network result might be returned so fast that
// flutter rebuilds again too quickly for you don't see the loading result on the stream
print([
result.source,
if (result.data != null) result.data['hero']['name']
]);
if (result.hasException) {
return Text(result.exception.toString());
}

if (result.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}

return Column(
children: <Widget>[
Text(getPrettyJSONString(result.data)),
if (result.isLoading)
const Center(
child: CircularProgressIndicator(),
),
if (result.data != null)
Text(
getPrettyJSONString(result.data),
),
RaisedButton(
onPressed: refetch,
onPressed: result.isNotLoading ? refetch : null,
child: const Text('REFETCH'),
),
],
Expand Down
58 changes: 45 additions & 13 deletions examples/starwars/lib/reviews/review.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ class Review {
Review({
@required this.episode,
@required this.stars,
@required this.id,
this.commentary,
});

String id;
Episode episode;
int stars;
String commentary;
Expand All @@ -20,6 +22,7 @@ class Review {
String commentary,
}) {
return Review(
id: id,
episode: episode ?? this.episode,
stars: stars ?? this.stars,
commentary: commentary ?? this.commentary,
Expand All @@ -37,6 +40,7 @@ class Review {
}

static Review fromJson(Map<String, dynamic> map) => Review(
id: map['id'],
episode: episodeFromJson(map['episode'] as String),
stars: map['stars'] as int,
commentary: map['commentary'] as String,
Expand All @@ -45,28 +49,56 @@ class Review {

const String Function(Object jsonObject) displayReview = getPrettyJSONString;

class DisplayReviews extends StatelessWidget {
class DisplayReviews extends StatefulWidget {
const DisplayReviews({
Key key,
@required this.reviews,
}) : super(key: key);

final List<Map<String, dynamic>> reviews;

@override
_DisplayReviewsState createState() => _DisplayReviewsState();
}

class _DisplayReviewsState extends State<DisplayReviews> {
List<Map<String, dynamic>> get reviews => widget.reviews;

Widget displayRaw(Map<String, dynamic> raw) => Card(
child: Container(
padding: const EdgeInsets.all(15.0),
//height: 150,
child: Text(displayReview(raw)),
),
);

/*
// for debugging
@override
initState() {
super.initState();
print(
'DisplayReviews.initState() called on $this.\n'
'this should only happen ONCE on this page, regardless of fetchMore calls, etc.',
);
}
@override
didUpdateWidget(old) {
super.didUpdateWidget(old);
print(
'DisplayReviews.didUpdateWidget() called on $this.\n'
'this can happen REPEATEDLY due to fetchMore, etc.',
);
}
*/

@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(8.0),
children: reviews
.map(displayReview)
.map<Widget>((String s) => Card(
child: Container(
padding: const EdgeInsets.all(15.0),
//height: 150,
child: Text(s),
),
))
.toList(),
return Container(
padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 0.0),
child: ListView(
children: reviews.map<Widget>(displayRaw).toList(),
),
);
}
}
2 changes: 1 addition & 1 deletion examples/starwars/lib/reviews/review_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import './review_subscription.dart';
class ReviewsPage extends StatelessWidget {
static const BottomNavigationBarItem navItem = BottomNavigationBarItem(
icon: Icon(Icons.star),
title: Text('Reviews'),
label: 'Reviews',
);

@override
Expand Down
4 changes: 2 additions & 2 deletions examples/starwars/lib/reviews/review_page_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:starwars_app/reviews/review.dart';
class PagingReviews extends StatelessWidget {
static const BottomNavigationBarItem navItem = BottomNavigationBarItem(
icon: Icon(Icons.description),
title: Text('Paging'),
label: 'Paging',
);

@override
Expand Down Expand Up @@ -73,7 +73,7 @@ class PagingReviews extends StatelessWidget {
),
);
},
child: Text('LOAD PAGE $nextPage'),
child: Text('LOAD PAGE ${nextPage + 1}'),
),
],
);
Expand Down
8 changes: 6 additions & 2 deletions packages/graphql/lib/src/core/observable_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,13 @@ class ObservableQuery {
return false;
}

/// Attempts to refetch, throwing error if not refetch safe
Future<QueryResult> refetch() async {
/// Attempts to refetch _on the network_, throwing error if not refetch safe
///
/// **NOTE:** overrides any present non-network-only [FetchPolicy],
/// as refetching from the `cache` does not make sense.
Future<QueryResult> refetch() {
if (isRefetchSafe) {
addResult(QueryResult.loading(data: latestResult.data));
return queryManager.refetchQuery(queryId);
}
throw Exception('Query is not refetch safe');
Expand Down
69 changes: 57 additions & 12 deletions packages/graphql/lib/src/core/policies.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import 'package:meta/meta.dart';
import "package:collection/collection.dart";

/// [FetchPolicy] determines where the client may return a result from. The options are:
/// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available.
/// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available.
/// - cacheOnly: return result from cache if available, fail otherwise.
/// - noCache: return result from network, fail if network call doesn't succeed, don't save to cache.
/// - networkOnly: return result from network, fail if network call doesn't succeed, save to cache.
/// [FetchPolicy] determines where the client may return a result from.
///
/// * [cacheFirst] (default): return result from cache. Only fetch from network if cached result is not available.
/// * [cacheAndNetwork]: return result from cache first (if it exists), then return network result once it's available.
/// * [cacheOnly]: return result from cache if available, fail otherwise.
/// * [noCache]: return result from network, fail if network call doesn't succeed, don't save to cache.
/// * [networkOnly]: return result from network, fail if network call doesn't succeed, save to cache.
///
/// The default `fetchPolicy` for each method are:
/// * `watchQuery`: [cacheAndNetwork]
/// * `query`: [cacheFirst]
/// * `mutation`: [networkOnly]
///
/// These can be overriden at client construction time by passing
/// a [DefaultPolicies] instance to `defaultPolicies`.
enum FetchPolicy {
/// Return result from cache. Only fetch from network if cached result is not available.
cacheFirst,

/// Return result from cache first (if it exists), then return network result once it's available.
cacheAndNetwork,

/// Return result from cache if available, fail otherwise.
cacheOnly,

/// Return result from network, fail if network call doesn't succeed, don't save to cache.
noCache,

/// Return result from network, fail if network call doesn't succeed, save to cache.
networkOnly,
}

Expand All @@ -26,15 +44,41 @@ bool shouldStopAtCache(FetchPolicy fetchPolicy) =>
fetchPolicy == FetchPolicy.cacheFirst ||
fetchPolicy == FetchPolicy.cacheOnly;

/// [ErrorPolicy] determines the level of events for errors in the execution result. The options are:
/// - none (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response.
/// - ignore: Ignore allows you to read any data that is returned alongside GraphQL Errors,
/// but doesn't save the errors or report them to your UI.
/// - all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server.
/// It saves both data and errors into the Apollo Cache so your UI can use them.
bool canExecuteOnNetwork(FetchPolicy policy) {
switch (policy) {
case FetchPolicy.noCache:
case FetchPolicy.networkOnly:
return true;
case FetchPolicy.cacheFirst:
case FetchPolicy.cacheAndNetwork:
case FetchPolicy.cacheOnly:
return false;
}
}

/// [ErrorPolicy] determines the level of events for errors in the execution result.
///
/// While the default for all client methods is [none],
/// [all] is recommended for notifying your users of potential issues.
///
/// * [none] (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response.
/// * [ignore]: Ignore allows you to read any data that is returned alongside GraphQL Errors,
/// but doesn't save the errors or report them to your UI.
/// * [all]: saves both data and errors into the `cache` so your UI can use them.
/// It is recommended for notifying your users of potential issues,
/// while still showing as much data as possible from your server.
enum ErrorPolicy {
/// Any GraphQL Errors are treated the same as network errors and any data is ignored from the response. (default)
none,

/// Ignore allows you to read any data that is returned alongside GraphQL Errors,
/// but doesn't save the errors or report them to your UI.
ignore,

/// saves both data and errors into the `cache` so your UI can use them.
///
/// It is recommended for notifying your users of potential issues,
/// while still showing as much data as possible from your server.
all,
}

Expand Down Expand Up @@ -143,6 +187,7 @@ class DefaultPolicies {
FetchPolicy.cacheFirst,
ErrorPolicy.none,
);

static final _mutateDefaults = Policies.safe(
FetchPolicy.networkOnly,
ErrorPolicy.none,
Expand Down
Loading

0 comments on commit 891bc2b

Please sign in to comment.