Skip to content

Commit

Permalink
feat: Use hooks
Browse files Browse the repository at this point in the history
Re-implement widgets using flutter_hooks and expose said hooks.
  • Loading branch information
budde377 authored and vincenzopalazzo committed Feb 14, 2022
1 parent 2437565 commit db9305a
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 299 deletions.
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
41 changes: 41 additions & 0 deletions packages/graphql_flutter/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

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

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

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
5 changes: 5 additions & 0 deletions packages/graphql_flutter/lib/graphql_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export 'package:graphql_flutter/src/widgets/query.dart';
export 'package:graphql_flutter/src/widgets/subscription.dart';
export 'package:graphql_flutter/src/widgets/result_accumulator.dart';

export 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
export 'package:graphql_flutter/src/widgets/hooks/query.dart';
export 'package:graphql_flutter/src/widgets/hooks/subscription.dart';
export 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';

export 'package:graphql_flutter/src/hive_init.dart';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';

import 'package:graphql/client.dart';
export 'package:graphql_flutter/src/widgets/hooks/query.dart';

class GraphQLProvider extends StatefulWidget {
const GraphQLProvider({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

GraphQLClient useGraphQLClient() {
final context = useContext();
return useValueListenable(GraphQLProvider.of(context));
}
53 changes: 53 additions & 0 deletions packages/graphql_flutter/lib/src/widgets/hooks/mutation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
import 'package:graphql_flutter/src/widgets/query.dart';

typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
Map<String, dynamic> variables, {
Object? optimisticResult,
});

class MutationHookResult<TParsed> {
final RunMutation<TParsed> runMutation;
final QueryResult<TParsed> result;

MutationHookResult({
required this.runMutation,
required this.result,
});
}

MutationHookResult<TParsed> useMutation<TParsed>(
MutationOptions<TParsed> options,
) {
final watchOptions = useMemoized(
() => options.asWatchQueryOptions(),
[options],
);
final client = useGraphQLClient();
final query = useWatchMutation<TParsed>(watchOptions);
final snapshot = useStream(
query.stream,
initialData: query.latestResult ?? QueryResult.unexecuted,
);
final runMutation = useCallback((
Map<String, dynamic> variables, {
Object? optimisticResult,
}) {
final mutationCallbacks = MutationCallbackHandler(
cache: client.cache,
queryId: query.queryId,
options: options,
);
return (query
..variables = variables
..optimisticResult = optimisticResult
..onData(mutationCallbacks.callbacks) // add callbacks to observable
)
.fetchResults();
}, [client, query, options]);
return MutationHookResult(
runMutation: runMutation,
result: snapshot.data!,
);
}
38 changes: 38 additions & 0 deletions packages/graphql_flutter/lib/src/widgets/hooks/query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';

// method to call from widget to fetchmore queries
typedef FetchMore<TParsed> = Future<QueryResult<TParsed>> Function(
FetchMoreOptions options);

typedef Refetch<TParsed> = Future<QueryResult<TParsed>?> Function();

class QueryHookResult<TParsed> {
final QueryResult<TParsed> result;
final Refetch<TParsed> refetch;
final FetchMore<TParsed> fetchMore;

QueryHookResult({
required this.result,
required this.refetch,
required this.fetchMore,
});
}

QueryHookResult<TParsed> useQuery<TParsed>(QueryOptions<TParsed> options) {
final watchQueryOptions = useMemoized(
() => options.asWatchQueryOptions(),
[options],
);
final query = useWatchQuery(watchQueryOptions);
final snapshot = useStream(
query.stream,
initialData: query.latestResult,
);
return QueryHookResult(
result: snapshot.data!,
refetch: query.refetch,
fetchMore: query.fetchMore,
);
}
129 changes: 129 additions & 0 deletions packages/graphql_flutter/lib/src/widgets/hooks/subscription.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'dart:async';
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';

typedef OnSubscriptionResult<TParsed> = void Function(
QueryResult<TParsed> subscriptionResult,
GraphQLClient? client,
);

typedef SubscriptionBuilder<TParsed> = Widget Function(
QueryResult<TParsed> result);

QueryResult<TParsed> useSubscription<TParsed>(
SubscriptionOptions<TParsed> options, {
OnSubscriptionResult<TParsed>? onSubscriptionResult,
}) {
final client = useGraphQLClient();
final stream = use(_SubscriptionHook(
client: client,
onSubscriptionResult: onSubscriptionResult,
options: options,
));
final snapshot = useStream(
stream,
initialData: options.optimisticResult != null
? QueryResult.optimistic(
data: options.optimisticResult as Map<String, dynamic>?,
parserFn: options.parserFn,
)
: QueryResult.loading(parserFn: options.parserFn),
);
return snapshot.data!;
}

class _SubscriptionHook<TParsed> extends Hook<Stream<QueryResult<TParsed>>> {
final SubscriptionOptions<TParsed> options;
final GraphQLClient client;
final OnSubscriptionResult<TParsed>? onSubscriptionResult;
_SubscriptionHook({
required this.options,
required this.client,
required this.onSubscriptionResult,
});
@override
HookState<Stream<QueryResult<TParsed>>, Hook<Stream<QueryResult<TParsed>>>>
createState() {
return _SubscriptionHookState();
}
}

class _SubscriptionHookState<TParsed> extends HookState<
Stream<QueryResult<TParsed>>, _SubscriptionHook<TParsed>> {
late Stream<QueryResult<TParsed>> stream;
GraphQLClient? client;

ConnectivityResult? _currentConnectivityResult;
StreamSubscription<ConnectivityResult>? _networkSubscription;

void _initSubscription() {
stream = client!.subscribe(hook.options);
final onSubscriptionResult = hook.onSubscriptionResult;
if (onSubscriptionResult != null) {
stream = stream.map((result) {
onSubscriptionResult(result, client);
return result;
});
}
}

@override
void initHook() {
super.initHook();
_networkSubscription =
Connectivity().onConnectivityChanged.listen(_onNetworkChange);
}

@override
void didUpdateHook(_SubscriptionHook<TParsed> oldHook) {
super.didUpdateHook(oldHook);

if (hook.options != oldHook.options || hook.client != oldHook.client) {
_initSubscription();
}
}

@override
void dispose() {
_networkSubscription?.cancel();
super.dispose();
}

Future _onNetworkChange(ConnectivityResult result) async {
//if from offline to online
if (_currentConnectivityResult == ConnectivityResult.none &&
(result == ConnectivityResult.mobile ||
result == ConnectivityResult.wifi)) {
_currentConnectivityResult = result;

// android connectivitystate cannot be trusted
// validate with nslookup
if (Platform.isAndroid) {
try {
final nsLookupResult = await InternetAddress.lookup('google.com');
if (nsLookupResult.isNotEmpty &&
nsLookupResult[0].rawAddress.isNotEmpty) {
_initSubscription();
}
// on exception -> no real connection, set current state to none
} on SocketException catch (_) {
_currentConnectivityResult = ConnectivityResult.none;
}
} else {
_initSubscription();
}
} else {
_currentConnectivityResult = result;
}
}

@override
Stream<QueryResult<TParsed>> build(BuildContext context) {
return stream;
}
}
95 changes: 95 additions & 0 deletions packages/graphql_flutter/lib/src/widgets/hooks/watch_query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';

class _WatchQueryHook<TParsed> extends Hook<ObservableQuery<TParsed>> {
final GraphQLClient client;
final WatchQueryOptions<TParsed> options;

_WatchQueryHook({
required this.options,
required this.client,
});

@override
HookState<ObservableQuery<TParsed>, Hook<ObservableQuery<TParsed>>>
createState() {
return _WatchQueryHookState();
}
}

class _WatchQueryHookState<TParsed>
extends HookState<ObservableQuery<TParsed>, _WatchQueryHook<TParsed>> {
late ObservableQuery<TParsed> _observableQuery;

@override
initHook() {
super.initHook();
_connect();
}

@override
dispose() {
_close();
super.dispose();
}

_connect() {
_observableQuery = hook.client.queryManager.watchQuery(hook.options);
}

_close() {
_observableQuery.close();
}

_reconnect() {
_close();
_connect();
}

@override
didUpdateHook(oldHook) {
super.didUpdateHook(oldHook);
if (oldHook.client == hook.client && oldHook.options == hook.options) {
return;
}
_reconnect();
}

ObservableQuery<TParsed> build(BuildContext context) {
return _observableQuery;
}
}

ObservableQuery<TParsed> useWatchQuery<TParsed>(
WatchQueryOptions<TParsed> options,
) {
final client = useGraphQLClient();
final overwrittenOptions = useMemoized(() {
final policies =
client.defaultPolicies.query.withOverrides(options.policies);
return options.copyWithPolicies(policies);
}, [options]);

return use(_WatchQueryHook(
options: overwrittenOptions,
client: client,
));
}

ObservableQuery<TParsed> useWatchMutation<TParsed>(
WatchQueryOptions<TParsed> options) {
final client = useGraphQLClient();
final overwrittenOptions = useMemoized(() {
final policies =
client.defaultPolicies.mutate.withOverrides(options.policies);
return options.copyWithPolicies(policies);
}, [options]);
return use(
_WatchQueryHook(
options: overwrittenOptions,
client: client,
),
);
}
Loading

0 comments on commit db9305a

Please sign in to comment.